import React, { useState, useEffect } from 'react'; import { createClient } from '@supabase/supabase-js'; import { Wallet, Receipt, CreditCard, History, Plus, Trash2, CheckCircle2, AlertCircle, Lock } from 'lucide-react'; // ========================================================================= // 1. CONFIGURACIÓN DE SUPABASE (Cambia esto con tus credenciales reales) // ========================================================================= const SUPABASE_URL = "TU_SUPABASE_URL_AQUI"; const SUPABASE_ANON_KEY = "TU_SUPABASE_ANON_KEY_AQUI"; const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); // ========================================================================= // 2. DEFINICIÓN DE TIPOS // ========================================================================= interface Bill { id: string; name: string; amount: number; period: 'Quincena 1' | 'Quincena 2'; } interface VirtualCard { id: string; name: string; monthly_budget: number; funding_type: 'Mensual único' | 'Dividido 50/50'; current_balance: number; } interface Transaction { id: string; card_id: string | null; description: string; amount: number; type: 'ingreso_q1' | 'ingreso_q2' | 'gasto_tarjeta' | 'pago_bill'; month_year: string; created_at: string; } // ========================================================================= // 3. COMPONENTE PRINCIPAL DEL DASHBOARD // ========================================================================= export default function QuincenaDashboard() { // Estados de la Aplicación const [bills, setBills] = useState([]); const [cards, setCards] = useState([]); const [transactions, setTransactions] = useState([]); const [isAdmin, setIsAdmin] = useState(false); const [loading, setLoading] = useState(true); const [userEmail, setUserEmail] = useState(''); // Estados para los estados de depósito (Persistidos desde la BD) const [q1Depositado, setQ1Depositado] = useState(false); const [q2Depositado, setQ2Depositado] = useState(false); // Estados para formularios de agregar nuevos elementos const [newBill, setNewBill] = useState({ name: '', amount: '', period: 'Quincena 1' as 'Quincena 1' | 'Quincena 2' }); const [newCard, setNewCard] = useState({ name: '', monthly_budget: '', funding_type: 'Dividido 50/50' as 'Mensual único' | 'Dividido 50/50' }); const [newExpense, setNewExpense] = useState({ cardId: '', description: '', amount: '' }); // Obtener el mes y año actual en formato YYYY-MM para la lógica quincenal const currentMonthYear = new Date().toISOString().slice(0, 7); // Cargar datos iniciales al montar la aplicación useEffect(() => { async function initApp() { try { // 1. Obtener sesión del usuario actual const { data: { session } } = await supabase.auth.getSession(); if (session?.user) { setUserEmail(session.user.email || ''); // Verificar rol en la tabla profiles const { data: profile } = await supabase .from('profiles') .select('role') .eq('id', session.user.id) .single(); setIsAdmin(profile?.role === 'admin'); } else { // Por seguridad en desarrollo local si no hay login, te dejamos como admin temporal setIsAdmin(true); } // 2. Descargar datos de la Base de Datos await refreshData(); } catch (error) { console.error("Error inicializando la app:", error); } finally { setLoading(false); } } initApp(); }, []); // Función global para refrescar los datos reflejando cambios en tiempo real async function refreshData() { const { data: dbBills } = await supabase.from('fixed_bills').select('*'); const { data: dbCards } = await supabase.from('virtual_cards').select('*'); const { data: dbTx } = await supabase.from('transactions').select('*').order('created_at', { ascending: false }); if (dbBills) setBills(dbBills); if (dbCards) setCards(dbCards); if (dbTx) { setTransactions(dbTx); // Derivar si las quincenas ya fueron depositadas este mes basándonos en el historial real setQ1Depositado(dbTx.some(t => t.type === 'ingreso_q1' && t.month_year === currentMonthYear)); setQ2Depositado(dbTx.some(t => t.type === 'ingreso_q2' && t.month_year === currentMonthYear)); } } // ========================================================================= // LOGICA DE NEGOCIO: PROCESAR QUINCENAS (MATEMÁTICAS EXACTAS) // ========================================================================= async function handleRegisterQ1() { if (!isAdmin || q1Depositado) return; setLoading(true); try { // 1. Insertar marcador de control en transactions para congelar la Q1 este mes await supabase.from('transactions').insert({ description: `💰 Depósito Cheque Quincena 1 - ${currentMonthYear}`, amount: 2700, type: 'ingreso_q1', month_year: currentMonthYear }); // 2. Pagar automáticamente los bills de la Q1 const q1Bills = bills.filter(b => b.period === 'Quincena 1'); for (const bill of q1Bills) { await supabase.from('transactions').insert({ description: `📉 Pago Automático: ${bill.name}`, amount: bill.amount, type: 'pago_bill', month_year: currentMonthYear }); } // 3. Fondear Tarjetas Virtuales según sus reglas for (const card of cards) { let extraFunding = 0; if (card.funding_type === 'Mensual único') { extraFunding = Number(card.monthly_budget); } else if (card.funding_type === 'Dividido 50/50') { extraFunding = Number(card.monthly_budget) / 2; } if (extraFunding > 0) { // Actualizar saldo de la tarjeta await supabase .from('virtual_cards') .update({ current_balance: Number(card.current_balance) + extraFunding }) .eq('id', card.id); // Guardar registro en el historial de la tarjeta await supabase.from('transactions').insert({ card_id: card.id, description: `🔋 Fondeo Q1 para ${card.name}`, amount: extraFunding, type: 'ingreso_q1', month_year: currentMonthYear }); } } await refreshData(); } catch (e) { console.error(e); } finally { setLoading(false); } } async function handleRegisterQ2() { if (!isAdmin || q2Depositado) return; setLoading(true); try { // 1. Insertar marcador para congelar la Q2 await supabase.from('transactions').insert({ description: `💰 Depósito Cheque Quincena 2 - ${currentMonthYear}`, amount: 2700, type: 'ingreso_q2', month_year: currentMonthYear }); // 2. Pagar automáticamente los bills de la Q2 (Incluye la Renta) const q2Bills = bills.filter(b => b.period === 'Quincena 2'); for (const bill of q2Bills) { await supabase.from('transactions').insert({ description: `📉 Pago Automático: ${bill.name}`, amount: bill.amount, type: 'pago_bill', month_year: currentMonthYear }); } // 3. Fondear el 50% restante únicamente a las tarjetas divididas const dividedCards = cards.filter(c => c.funding_type === 'Dividido 50/50'); for (const card of dividedCards) { const extraFunding = Number(card.monthly_budget) / 2; await supabase .from('virtual_cards') .update({ current_balance: Number(card.current_balance) + extraFunding }) .eq('id', card.id); await supabase.from('transactions').insert({ card_id: card.id, description: `🔋 Fondeo Q2 (50% Restante) para ${card.name}`, amount: extraFunding, type: 'ingreso_q2', month_year: currentMonthYear }); } await refreshData(); } catch (e) { console.error(e); } finally { setLoading(false); } } // Restar un gasto real hecho con alguna de tus tarjetas virtuales async function handleAddExpense(e: React.FormEvent) { e.preventDefault(); if (!isAdmin || !newExpense.cardId || !newExpense.amount) return; const selectedCard = cards.find(c => c.id === newExpense.cardId); if (!selectedCard) return; const expenseAmount = Number(newExpense.amount); // 1. Restar el dinero del saldo disponible de la tarjeta await supabase .from('virtual_cards') .update({ current_balance: Number(selectedCard.current_balance) - expenseAmount }) .eq('id', selectedCard.id); // 2. Registrar el gasto en el historial de transacciones await supabase.from('transactions').insert({ card_id: selectedCard.id, description: newExpense.description || `Gasto en ${selectedCard.name}`, amount: expenseAmount, type: 'gasto_tarjeta', month_year: currentMonthYear }); setNewExpense({ cardId: '', description: '', amount: '' }); await refreshData(); } // Agregar nuevos Bills o Tarjetas desde el panel de Configuración async function handleAddBill(e: React.FormEvent) { e.preventDefault(); if (!isAdmin || !newBill.name || !newBill.amount) return; await supabase.from('fixed_bills').insert({ name: newBill.name, amount: Number(newBill.amount), period: newBill.period }); setNewBill({ name: '', amount: '', period: 'Quincena 1' }); await refreshData(); } async function handleAddCard(e: React.FormEvent) { e.preventDefault(); if (!isAdmin || !newCard.name || !newCard.monthly_budget) return; await supabase.from('virtual_cards').insert({ name: newCard.name, monthly_budget: Number(newCard.monthly_budget), funding_type: newCard.funding_type, current_balance: 0 }); setNewCard({ name: '', monthly_budget: '', funding_type: 'Dividido 50/50' }); await refreshData(); } // Funciones para eliminar conceptos async function handleDeleteBill(id: string) { if (!isAdmin) return; await supabase.from('fixed_bills').delete().eq('id', id); await refreshData(); } async function handleDeleteCard(id: string) { if (!isAdmin) return; await supabase.from('virtual_cards').delete().eq('id', id); await refreshData(); } // Reiniciar ciclo del simulador (Limpieza para pruebas o nuevo mes) async function handleResetCycle() { if (!isAdmin) return; if (confirm("¿Estás seguro de que deseas limpiar los registros de este mes? Esto borrará las transacciones del mes actual.")) { await supabase.from('transactions').delete().eq('month_year', currentMonthYear); // Resetear balances de tarjetas a 0 para simular de nuevo limpio await supabase.from('virtual_cards').update({ current_balance: 0 }).neq('id', '00000000-0000-0000-0000-000000000000'); await refreshData(); } } // ========================================================================= // CÁLCULOS MATEMÁTICOS PARA LA VISTA PREVIA (SIMULADOR) // ========================================================================= const q1BillsTotal = bills.filter(b => b.period === 'Quincena 1').reduce((acc, b) => acc + Number(b.amount), 0); const q2BillsTotal = bills.filter(b => b.period === 'Quincena 2').reduce((acc, b) => acc + Number(b.amount), 0); const q1CardsTotal = cards.reduce((acc, c) => { return acc + (c.funding_type === 'Mensual único' ? Number(c.monthly_budget) : Number(c.monthly_budget) / 2); }, 0); const q2CardsTotal = cards.reduce((acc, c) => { return acc + (c.funding_type === 'Dividido 50/50' ? Number(c.monthly_budget) / 2 : 0); }, 0); const remanenteQ1 = 2700 - q1BillsTotal - q1CardsTotal; const remanenteQ2 = 2700 - q2BillsTotal - q2CardsTotal; if (loading) { return (
Cargando ecosistema financiero...
); } return (
{/* HEADER DE LA APLICACIÓN */}

Flujo de Caja Quincenal

Ledger Quincenal

Dos cheques de $2,700 al mes. Automatización de sobres y blindaje de renta.

Ingreso Mensual Fijo $5,400.00
{/* MODO SOLO LECTURA WARNING */} {!isAdmin && (
Modo de Vista Protegido: Estás conectado como lector (`{userEmail}`). Solo el administrador puede realizar modificaciones o registrar quincenas para evitar errores accidentales.
)}
{/* COLUMNA IZQUIERDA: CONFIGURADOR DE BILLS FIJOS */}

Bills Fijos Mensuales

{bills.map(bill => (

{bill.name}

{bill.period}
${Number(bill.amount).toFixed(2)} {isAdmin && ( )}
))}
{isAdmin && (
setNewBill({...newBill, name: e.target.value})} className="bg-[#0B0F17] border border-gray-800 rounded-lg px-3 py-2 text-xs focus:outline-none focus:border-amber-500 text-white" /> setNewBill({...newBill, amount: e.target.value})} className="bg-[#0B0F17] border border-gray-800 rounded-lg px-3 py-2 text-xs focus:outline-none focus:border-amber-500 text-white" />
)}
{/* COLUMNA DERECHA: SECCIÓN DE TARJETAS VIRTUALES (SOBRES DIGITALES) */}

Tarjetas Virtuales (Sobres)

{cards.map(card => { const porcentaje = Math.min(100, Math.max(0, (card.current_balance / card.monthly_budget) * 100)); return (

{card.name}

{card.funding_type}
Presupuesto: ${card.monthly_budget} Disponible: ${Number(card.current_balance).toFixed(2)}
{isAdmin && ( )}
{/* Barra de progreso visual */}
); })}
{isAdmin && (
setNewCard({...newCard, name: e.target.value})} className="bg-[#0B0F17] border border-gray-800 rounded-lg px-3 py-2 text-xs focus:outline-none focus:border-amber-500 text-white" /> setNewCard({...newCard, monthly_budget: e.target.value})} className="bg-[#0B0F17] border border-gray-800 rounded-lg px-3 py-2 text-xs focus:outline-none focus:border-amber-500 text-white" />
)}
{/* SECCIÓN INFERIOR COMPLETA: SIMULADOR DE FLUJO Q1 VS Q2 */}

Simulador - Proyección Q1 vs Q2 ({currentMonthYear})

{isAdmin && ( )}
{/* VISTA QUINCENA 1 */}

Quincena 1

{q1Depositado ? '✓ DEPOSITADO' : 'PENDIENTE'}
Ingreso (Cheque fijo)$2,700.00
Bills fijos de Q1-${q1BillsTotal.toFixed(2)}
Tarjetas - Fondeo Mensual Único (100%)-${cards.filter(c => c.funding_type === 'Mensual único').reduce((a,c)=>a+Number(c.monthly_budget),0).toFixed(2)}
Tarjetas - Fondeo Dividido (50% Inicial)-${cards.filter(c => c.funding_type === 'Dividido 50/50').reduce((a,c)=>a+(Number(c.monthly_budget)/2),0).toFixed(2)}
Remanente Neto (Sobrante Real) ${remanenteQ1.toFixed(2)}
{/* VISTA QUINCENA 2 (PROTECCIÓN DE LA RENTA) */}

Quincena 2

{q2Depositado ? '✓ DEPOSITADO' : 'PENDIENTE'}
Ingreso (Cheque fijo)$2,700.00
Bills fijos de Q2 (Incl. Renta)-${q2BillsTotal.toFixed(2)}
Tarjetas - Fondeo Mensual Único-$0.00 (Ya fondeadas en Q1)
Tarjetas - Fondeo Dividido (50% Restante)-${q2CardsTotal.toFixed(2)}
Remanente Neto (Sobrante Real) ${remanenteQ2.toFixed(2)}
{/* SECCIÓN GASTOS REALES EXTRA (PARA DESCONTAR DÍA A DÍA) */} {isAdmin && cards.length > 0 && (

Debitar Gasto Diario de Tarjeta Virtual

setNewExpense({...newExpense, description: e.target.value})} className="bg-[#0B0F17] border border-gray-800 rounded-xl px-3 py-2.5 text-xs text-white focus:outline-none focus:border-amber-500" /> setNewExpense({...newExpense, amount: e.target.value})} className="bg-[#0B0F17] border border-gray-800 rounded-xl px-3 py-2.5 text-xs text-white focus:outline-none focus:border-amber-500" />
)}
); }