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:
196
components/modals/AddCardModal.tsx
Normal file
196
components/modals/AddCardModal.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { X, CreditCard, Calendar, DollarSign, Palette } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AddCardModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
{ name: 'Slate', value: '#64748b' },
|
||||
{ name: 'Blue', value: '#3b82f6' },
|
||||
{ name: 'Cyan', value: '#06b6d4' },
|
||||
{ name: 'Emerald', value: '#10b981' },
|
||||
{ name: 'Violet', value: '#8b5cf6' },
|
||||
{ name: 'Rose', value: '#f43f5e' },
|
||||
{ name: 'Amber', value: '#f59e0b' },
|
||||
]
|
||||
|
||||
export function AddCardModal({ isOpen, onClose }: AddCardModalProps) {
|
||||
const addCreditCard = useFinanzasStore((state) => state.addCreditCard)
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [lastFour, setLastFour] = useState('')
|
||||
const [limit, setLimit] = useState('')
|
||||
const [closingDay, setClosingDay] = useState('')
|
||||
const [dueDay, setDueDay] = useState('')
|
||||
const [selectedColor, setSelectedColor] = useState(COLORS[1].value)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!name || !limit || !closingDay || !dueDay) return
|
||||
|
||||
addCreditCard({
|
||||
name,
|
||||
lastFourDigits: lastFour || '****',
|
||||
closingDay: parseInt(closingDay),
|
||||
dueDay: parseInt(dueDay),
|
||||
currentBalance: 0,
|
||||
creditLimit: parseFloat(limit),
|
||||
color: selectedColor
|
||||
})
|
||||
|
||||
// Reset
|
||||
setName('')
|
||||
setLastFour('')
|
||||
setLimit('')
|
||||
setClosingDay('')
|
||||
setDueDay('')
|
||||
setSelectedColor(COLORS[1].value)
|
||||
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||
<div className="w-full max-w-lg rounded-xl bg-slate-900 border border-slate-800 shadow-2xl overflow-hidden scale-100 animate-in zoom-in-95 duration-200">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-slate-800">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<CreditCard className="text-cyan-500" /> Nueva Tarjeta
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
||||
|
||||
{/* Name & Last 4 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Nombre Banco / Tarjeta</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: Visa Santander"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Ult. 4 Dig.</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={4}
|
||||
placeholder="1234"
|
||||
value={lastFour}
|
||||
onChange={(e) => setLastFour(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none text-center tracking-widest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Limit */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Límite de Crédito</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-semibold">$</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
value={limit}
|
||||
onChange={(e) => setLimit(e.target.value)}
|
||||
className="w-full pl-8 pr-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white text-lg font-mono outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Closing Day */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Día Cierre</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
placeholder="20"
|
||||
value={closingDay}
|
||||
onChange={(e) => setClosingDay(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none"
|
||||
required
|
||||
/>
|
||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 text-sm">del mes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Due Day */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Día Vencimiento</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
placeholder="5"
|
||||
value={dueDay}
|
||||
onChange={(e) => setDueDay(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none"
|
||||
required
|
||||
/>
|
||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 text-sm">del mes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Color Picker */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Palette size={12} /> Color de Tarjeta
|
||||
</label>
|
||||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||
{COLORS.map((color) => (
|
||||
<button
|
||||
key={color.value}
|
||||
type="button"
|
||||
onClick={() => setSelectedColor(color.value)}
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full border-2 transition-all",
|
||||
selectedColor === color.value
|
||||
? "border-white scale-110 shadow-lg"
|
||||
: "border-transparent opacity-70 hover:opacity-100 hover:scale-105"
|
||||
)}
|
||||
style={{ backgroundColor: color.value }}
|
||||
title={color.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 bg-cyan-500 hover:bg-cyan-400 text-white font-semibold rounded-lg shadow-lg shadow-cyan-500/20 transition-all active:scale-[0.98]"
|
||||
>
|
||||
Crear Tarjeta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
232
components/modals/AddDebtModal.tsx
Normal file
232
components/modals/AddDebtModal.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { X, Calendar, DollarSign, Tag, FileText, CheckCircle2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AddDebtModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type DebtType = 'fixed' | 'variable'
|
||||
|
||||
export function AddDebtModal({ isOpen, onClose }: AddDebtModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<DebtType>('variable')
|
||||
const [name, setName] = useState('')
|
||||
const [amount, setAmount] = useState('')
|
||||
const [dateStr, setDateStr] = useState(new Date().toISOString().split('T')[0]) // For variable: YYYY-MM-DD
|
||||
const [dueDay, setDueDay] = useState('1') // For fixed: 1-31
|
||||
const [categoryFixed, setCategoryFixed] = useState('housing')
|
||||
const [categoryVariable, setCategoryVariable] = useState('shopping')
|
||||
const [isAutoDebit, setIsAutoDebit] = useState(false)
|
||||
const [notes, setNotes] = useState('')
|
||||
|
||||
const addFixedDebt = useFinanzasStore((state) => state.addFixedDebt)
|
||||
const addVariableDebt = useFinanzasStore((state) => state.addVariableDebt)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!name || !amount) return
|
||||
|
||||
const numAmount = parseFloat(amount)
|
||||
if (isNaN(numAmount)) return
|
||||
|
||||
if (activeTab === 'fixed') {
|
||||
addFixedDebt({
|
||||
name,
|
||||
amount: numAmount,
|
||||
dueDay: parseInt(dueDay),
|
||||
category: categoryFixed as any,
|
||||
isAutoDebit,
|
||||
isPaid: false,
|
||||
notes: notes || undefined
|
||||
})
|
||||
} else {
|
||||
addVariableDebt({
|
||||
name,
|
||||
amount: numAmount,
|
||||
date: new Date(dateStr).toISOString(),
|
||||
category: categoryVariable as any,
|
||||
isPaid: false,
|
||||
notes: notes || undefined
|
||||
})
|
||||
}
|
||||
|
||||
// Reset and Close
|
||||
setName('')
|
||||
setAmount('')
|
||||
setNotes('')
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||
<div className="w-full max-w-lg rounded-xl bg-slate-900 border border-slate-800 shadow-2xl overflow-hidden scale-100 animate-in zoom-in-95 duration-200">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-slate-800">
|
||||
<h2 className="text-xl font-semibold text-white">Agregar Gasto / Deuda</h2>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex p-1 mx-6 mt-6 bg-slate-800/50 rounded-lg">
|
||||
<button
|
||||
onClick={() => setActiveTab('variable')}
|
||||
className={cn(
|
||||
"flex-1 py-2 text-sm font-medium rounded-md transition-all duration-200",
|
||||
activeTab === 'variable' ? "bg-cyan-500 text-white shadow-lg" : "text-slate-400 hover:text-white"
|
||||
)}
|
||||
>
|
||||
Variable (Único)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('fixed')}
|
||||
className={cn(
|
||||
"flex-1 py-2 text-sm font-medium rounded-md transition-all duration-200",
|
||||
activeTab === 'fixed' ? "bg-cyan-500 text-white shadow-lg" : "text-slate-400 hover:text-white"
|
||||
)}
|
||||
>
|
||||
Fijo (Recurrente)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Monto</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-semibold">$</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="w-full pl-8 pr-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white text-lg font-mono outline-none transition-all placeholder:text-slate-600"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name Input */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Descripción</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: Supermercado Coto, Netflix, Alquiler"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none transition-all placeholder:text-slate-600"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
{/* Category Select */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-1">
|
||||
<Tag size={12} /> Categoría
|
||||
</label>
|
||||
<select
|
||||
value={activeTab === 'fixed' ? categoryFixed : categoryVariable}
|
||||
onChange={(e) => activeTab === 'fixed' ? setCategoryFixed(e.target.value) : setCategoryVariable(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none appearance-none cursor-pointer"
|
||||
>
|
||||
{activeTab === 'fixed' ? (
|
||||
<>
|
||||
<option value="housing">Vivienda</option>
|
||||
<option value="services">Servicios</option>
|
||||
<option value="subscription">Suscripciones</option>
|
||||
<option value="other">Otro</option>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<option value="food">Comida / Super</option>
|
||||
<option value="shopping">Compras</option>
|
||||
<option value="transport">Transporte</option>
|
||||
<option value="health">Salud</option>
|
||||
<option value="entertainment">Entretenimiento</option>
|
||||
<option value="other">Otro</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Date/DueDay Input */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-1">
|
||||
<Calendar size={12} /> {activeTab === 'fixed' ? 'Día Vencimiento' : 'Fecha'}
|
||||
</label>
|
||||
{activeTab === 'fixed' ? (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
value={dueDay}
|
||||
onChange={(e) => setDueDay(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none"
|
||||
required
|
||||
/>
|
||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 text-sm">del mes</span>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="date"
|
||||
value={dateStr}
|
||||
onChange={(e) => setDateStr(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none [color-scheme:dark]"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'fixed' && (
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-slate-800/30 rounded-lg cursor-pointer" onClick={() => setIsAutoDebit(!isAutoDebit)}>
|
||||
<div className={cn("w-5 h-5 rounded border flex items-center justify-center transition-colors", isAutoDebit ? "bg-cyan-500 border-cyan-500" : "border-slate-600 bg-transparent")}>
|
||||
{isAutoDebit && <CheckCircle2 size={14} className="text-white" />}
|
||||
</div>
|
||||
<span className="text-sm text-slate-300 select-none">Débito Automático</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-1">
|
||||
<FileText size={12} /> Notas (Opcional)
|
||||
</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Detalles adicionales..."
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none min-h-[80px] text-sm resize-none placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 bg-cyan-500 hover:bg-cyan-400 text-white font-semibold rounded-lg shadow-lg shadow-cyan-500/20 transition-all active:scale-[0.98]"
|
||||
>
|
||||
Agregar {activeTab === 'fixed' ? 'Gasto Fijo' : 'Gasto'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
203
components/modals/AddPaymentModal.tsx
Normal file
203
components/modals/AddPaymentModal.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { X, CreditCard, DollarSign, Calendar, FileText, Layers } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface AddPaymentModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function AddPaymentModal({ isOpen, onClose }: AddPaymentModalProps) {
|
||||
const cards = useFinanzasStore((state) => state.creditCards)
|
||||
const addCardPayment = useFinanzasStore((state) => state.addCardPayment)
|
||||
|
||||
const [selectedCardId, setSelectedCardId] = useState(cards[0]?.id || '')
|
||||
const [description, setDescription] = useState('')
|
||||
const [amount, setAmount] = useState('')
|
||||
const [dateStr, setDateStr] = useState(new Date().toISOString().split('T')[0])
|
||||
const [hasInstallments, setHasInstallments] = useState(false)
|
||||
const [installments, setInstallments] = useState('1')
|
||||
const [totalInstallments, setTotalInstallments] = useState('12')
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
// Ensure card selection if cards exist
|
||||
if (!selectedCardId && cards.length > 0) {
|
||||
setSelectedCardId(cards[0].id)
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!description || !amount || !selectedCardId) return
|
||||
|
||||
addCardPayment({
|
||||
cardId: selectedCardId,
|
||||
amount: parseFloat(amount),
|
||||
date: new Date(dateStr).toISOString(),
|
||||
description,
|
||||
installments: hasInstallments ? {
|
||||
current: parseInt(installments),
|
||||
total: parseInt(totalInstallments)
|
||||
} : undefined
|
||||
})
|
||||
|
||||
// Reset
|
||||
setDescription('')
|
||||
setAmount('')
|
||||
setHasInstallments(false)
|
||||
setInstallments('1')
|
||||
setTotalInstallments('12')
|
||||
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||
<div className="w-full max-w-lg rounded-xl bg-slate-900 border border-slate-800 shadow-2xl overflow-hidden scale-100 animate-in zoom-in-95 duration-200">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-slate-800">
|
||||
<h2 className="text-xl font-semibold text-white">Registrar Consumo / Pago</h2>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{cards.length === 0 ? (
|
||||
<div className="p-8 text-center space-y-4">
|
||||
<CreditCard className="mx-auto text-slate-600 mb-2" size={48} />
|
||||
<h3 className="text-lg font-medium text-white">No tienes tarjetas registradas</h3>
|
||||
<p className="text-slate-400">Debes agregar una tarjeta antes de registrar pagos.</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg transition"
|
||||
>
|
||||
Entendido
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
||||
|
||||
{/* Card Selection */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Tarjeta</label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[120px] overflow-y-auto pr-1">
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.id}
|
||||
onClick={() => setSelectedCardId(card.id)}
|
||||
className={cn(
|
||||
"cursor-pointer p-3 rounded-lg border flex items-center gap-3 transition-all",
|
||||
selectedCardId === card.id
|
||||
? "border-cyan-500 bg-cyan-500/10 ring-1 ring-cyan-500"
|
||||
: "border-slate-800 bg-slate-950 hover:border-slate-700"
|
||||
)}
|
||||
>
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: card.color }} />
|
||||
<div className="flex flex-col truncate">
|
||||
<span className="text-sm font-medium text-white truncate">{card.name}</span>
|
||||
<span className="text-xs text-slate-500">**** {card.lastFourDigits}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Monto</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-semibold">$</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="w-full pl-8 pr-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white text-lg font-mono outline-none"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Descripción</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: Cena McDonalds, Compra ML"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-1">
|
||||
<Calendar size={12} /> Fecha
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateStr}
|
||||
onChange={(e) => setDateStr(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white outline-none [color-scheme:dark]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Installments Toggle */}
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-slate-800/30 rounded-lg cursor-pointer" onClick={() => setHasInstallments(!hasInstallments)}>
|
||||
<div className={cn("w-5 h-5 rounded border flex items-center justify-center transition-colors", hasInstallments ? "bg-cyan-500 border-cyan-500" : "border-slate-600 bg-transparent")}>
|
||||
{hasInstallments && <Layers size={14} className="text-white" />}
|
||||
</div>
|
||||
<span className="text-sm text-slate-300 select-none">Es una compra en cuotas</span>
|
||||
</div>
|
||||
|
||||
{/* Installments Inputs */}
|
||||
{hasInstallments && (
|
||||
<div className="grid grid-cols-2 gap-4 animate-in slide-in-from-top-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Cuota N°</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={installments}
|
||||
onChange={(e) => setInstallments(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Total Cuotas</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={totalInstallments}
|
||||
onChange={(e) => setTotalInstallments(e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 bg-cyan-500 hover:bg-cyan-400 text-white font-semibold rounded-lg shadow-lg shadow-cyan-500/20 transition-all active:scale-[0.98]"
|
||||
>
|
||||
Registrar Pago
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user