<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calculadora de Retiro Paso a Paso con PDF Mejorado</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<!-- jsPDF Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<!-- html2canvas Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #eef2f7;
}
.container-card {
background-color: white;
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.12);
width: 100%;
max-width: 800px;
margin: 2.5rem auto;
}
.main-title {
font-size: 2rem;
sm:font-size: 2.25rem;
font-weight: 700;
text-align: center;
color: #2c5282;
margin-bottom: 1rem;
}
.step-title {
font-size: 1.5rem;
font-weight: 600;
color: #374151;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #e2e8f0;
}
.sub-section-title {
font-size: 1.2rem;
font-weight: 600;
color: #4a5568; /* Darker gray */
margin-top: 2rem;
margin-bottom: 1rem;
padding-bottom: 0.25rem;
border-bottom: 1px dashed #cbd5e0;
}
.input-group, .radio-group-container {
margin-bottom: 1.5rem;
}
.input-label, .radio-group-label {
display: block;
margin-bottom: 0.625rem;
font-weight: 500;
color: #4a5568;
font-size: 0.9rem;
}
.input-field {
width: 100%;
padding: 0.875rem 1rem;
border: 1px solid #cbd5e0;
border-radius: 0.5rem;
box-shadow: inset 0 1px 3px 0 rgba(0, 0, 0, 0.07);
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.input-field:focus {
outline: 2px solid transparent;
outline-offset: 2px;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.3);
}
.btn-primary, .btn-secondary, .btn-success {
padding: 0.875rem 1.75rem;
border-radius: 0.5rem;
font-weight: 600;
transition: all 0.3s ease;
font-size: 1rem;
cursor: pointer;
}
.btn-primary {
color: white;
background-image: linear-gradient(to right, #4a90e2 0%, #2c5282 100%);
box-shadow: 0 4px 15px 0 rgba(74, 144, 226, 0.35);
}
.btn-primary:hover {
background-image: linear-gradient(to right, #2c5282 0%, #4a90e2 100%);
box-shadow: 0 6px 20px 0 rgba(74, 144, 226, 0.45);
transform: translateY(-1px);
}
.btn-secondary {
color: #2c5282;
background-color: #e2e8f0;
border: 1px solid #cbd5e0;
}
.btn-secondary:hover {
background-color: #cbd5e0;
}
.btn-success {
color: white;
background-color: #38a169; /* Green */
box-shadow: 0 4px 15px 0 rgba(56, 161, 105, 0.35);
}
.btn-success:hover {
background-color: #2f855a; /* Darker Green */
box-shadow: 0 6px 20px 0 rgba(56, 161, 105, 0.45);
transform: translateY(-1px);
}
.btn-success:disabled {
background-color: #a0aec0;
box-shadow: none;
cursor: not-allowed;
}
.results-summary-card, .chart-card, .boosters-card, .step-card {
background-color: #ffffff;
padding: 2rem;
border-radius: 0.75rem;
box-shadow: 0 8px 16px -4px rgba(0,0,0,0.08);
margin-top: 1.5rem;
}
.section-title {
font-size: 1.3rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 1.5rem;
text-align: center;
}
.summary-section { /* Used in Step 5 for each scenario block */
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #e2e8f0; /* Lighter border for sections */
}
.summary-section:last-of-type { /* No border for the very last section */
border-bottom: none;
padding-bottom: 0.5rem;
}
.summary-title { /* For titles like "Escenario 1: ..." */
font-weight: 700; /* Bolder */
color: #2c5282; /* Main blue */
font-size: 1.25rem; /* Slightly larger */
margin-bottom: 1rem;
}
.summary-item { /* For each line item like "Ahorros Iniciales: $X" */
display: flex;
justify-content: space-between;
font-size: 1rem; /* Larger for readability */
color: #4a5568;
padding: 0.375rem 0; /* More vertical padding */
}
.summary-item span:first-child { /* Label part */
color: #1a202c; /* Darker text for labels */
}
.summary-item strong { /* Value part */
color: #2c5282; /* Main blue for values */
font-weight: 600;
}
.summary-total-final { /* For the grand total in Step 5 */
font-weight: bold;
font-size: 1.2rem; /* Larger total */
color: #166534; /* Dark Green for final positive outcome */
margin-top: 0.75rem;
border-top: 2px solid #38a169; /* Green top border */
padding-top: 0.75rem;
}
.initial-summary-value { /* Specific style for Step 3 monto final */
font-size: 1.5rem; /* Larger */
font-weight: 700;
color: #2c5282;
text-align: center;
margin-top: 0.5rem;
}
.strategy-change {
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin: 0.75rem 0;
color: #4a5568;
}
.strategy-change .fas {
margin: 0 0.75rem;
color: #2c5282;
}
#chartContainerInitial, #chartContainerFinal { height: 350px; position: relative; }
@media (min-width: 768px) { #chartContainerInitial, #chartContainerFinal { height: 400px; } }
.chart-placeholder-text { color: #718096; font-style: italic; font-size: 1rem; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); }
.modal-content { background-color: #ffffff; margin: 10% auto; padding: 30px; border: none; width: 90%; max-width: 480px; border-radius: 0.75rem; text-align: center; box-shadow: 0 10px 30px rgba(0,0,0,0.2); }
.modal-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #e53e3e; }
.modal-body { margin-bottom: 2rem; color: #4a5568; font-size: 1.05rem; }
.modal-button { background-color: #4299e1; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 0.5rem; cursor: pointer; font-weight: 500; font-size: 1rem; transition: background-color 0.2s ease; }
.modal-button:hover { background-color: #3182ce; }
.info-text { font-size: 0.9rem; color: #4a5568; margin-top: 1rem; text-align: center; padding: 0.5rem; background-color: #f0f4f8; border-radius: 0.375rem; }
.booster-option { display: flex; align-items: center; justify-content: space-between; padding: 1rem; border: 1px solid #e2e8f0; border-radius: 0.5rem; margin-bottom: 1rem; transition: all 0.2s ease; }
.booster-option:hover { border-color: #4299e1; background-color: #f7fafc; }
.booster-option input[type="checkbox"] { appearance: none; width: 1.5rem; height: 1.5rem; border: 2px solid #cbd5e0; border-radius: 0.25rem; margin-right: 1rem; cursor: pointer; position: relative; transition: all 0.2s ease; }
.booster-option input[type="checkbox"]:checked { background-color: #4299e1; border-color: #4299e1; }
.booster-option input[type="checkbox"]:checked::before { content: '\f00c'; font-family: 'Font Awesome 6 Free'; font-weight: 900; color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 0.9rem; }
.booster-label-container { display: flex; align-items: center; flex-grow: 1; }
.booster-name { font-weight: 600; color: #2d3748; }
.booster-saving { font-size: 0.9rem; color: #48bb78; }
.booster-final-impact { font-size: 0.9rem; color: #2c5282; font-weight: 600; margin-left: auto; white-space: nowrap; }
.total-booster-summary { text-align: right; margin-top: 1rem; }
.total-booster-summary > div { margin-bottom: 0.25rem; }
.total-booster-summary strong { color: #2c5282; }
.radio-option { display: flex; align-items: center; padding: 1rem; border: 1px solid #e2e8f0; border-radius: 0.5rem; margin-bottom: 0.75rem; cursor: pointer; transition: all 0.2s ease; }
.radio-option:hover { border-color: #4299e1; background-color: #f7fafc; }
.radio-option input[type="radio"] { appearance: none; width: 1.5rem; height: 1.5rem; border: 2px solid #cbd5e0; border-radius: 50%; margin-right: 1rem; position: relative; transition: all 0.2s ease; }
.radio-option input[type="radio"]:checked { border-color: #4299e1; }
.radio-option input[type="radio"]:checked::before { content: ''; display: block; width: 0.875rem; height: 0.875rem; background-color: #4299e1; border-radius: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
.radio-label-text { font-weight: 500; color: #2d3748; }
.future-strategy-preview {
font-size: 0.9rem;
color: #2c5282;
font-weight: 600;
margin-left: 0.5rem;
}
.step-navigation { display: flex; justify-content: space-between; align-items: center; margin-top: 2rem; }
.step-card { display: none; }
.step-card.active { display: block; }
</style>
</head>
<body>
<!-- Calculator content will be managed by JavaScript based on auth state -->
<!-- This div will be shown if user is NOT authenticated -->
<!-- <div id="authContainer" class="container-card text-center">
<h1 class="main-title mb-8">Bienvenido a tu Calculadora de Retiro</h1>
<p class="text-lg text-gray-700 mb-6">Inicia sesión con Google para comenzar a planificar tu futuro.</p>
<button id="signInButton" class="btn-google text-lg px-8 py-3">
<i class="fab fa-google mr-3"></i>Iniciar Sesión con Google
</button>
</div> -->
<!-- This div will be shown AFTER user is authenticated -->
<div id="calculatorContainer" class=""> <!-- Initially visible, no 'hidden' class -->
<!-- <div class="flex justify-between items-center p-4 bg-white shadow-md mb-6 rounded-lg max-w-4xl mx-auto mt-4">
<div id="userProfile" class="text-sm text-gray-700 hidden">
Hola, <span id="userName" class="font-semibold"></span>
</div>
<button id="signOutButton" class="btn-secondary !py-2 !px-4 text-sm hidden">Cerrar Sesión</button>
</div> -->
<div class="container-card">
<h1 class="main-title">Calculadora de Retiro Paso a Paso</h1>
<div id="ageInfoGlobal" class="info-text mb-6"></div>
<!-- Step 1: Personal Data -->
<div id="step1" class="step-card active">
<h2 class="step-title"><i class="fas fa-user-edit mr-2"></i>Paso 1: Tus Datos Personales</h2>
<form id="step1Form">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4">
<div class="input-group">
<label for="fullName" class="input-label">Nombre Completo:</label>
<input type="text" id="fullName" class="input-field" placeholder="Ej: Ana Pérez" required>
</div>
<div class="input-group">
<label for="email" class="input-label">Correo Electrónico:</label>
<input type="email" id="email" class="input-field" placeholder="Ej: ana.perez@correo.com" required>
</div>
<div class="input-group">
<label for="dob" class="input-label">Fecha de Nacimiento:</label>
<input type="date" id="dob" class="input-field" required>
</div>
<div class="input-group">
<label for="retirementAge" class="input-label">Edad de Retiro Deseada:</label>
<input type="number" id="retirementAge" class="input-field" placeholder="Ej: 65" min="1" required>
</div>
</div>
<div class="input-group mt-4">
<label for="currentSavings" class="input-label">Ahorros Actuales ($):</label>
<input type="number" id="currentSavings" class="input-field" placeholder="Ej: 10.000" min="0" step="any" required>
</div>
<div class="text-right mt-6">
<button type="submit" class="btn-primary">Siguiente Paso <i class="fas fa-arrow-right ml-2"></i></button>
</div>
</form>
</div>
<!-- Step 2: Current Investment Strategy -->
<div id="step2" class="step-card">
<h2 class="step-title"><i class="fas fa-piggy-bank mr-2"></i>Paso 2: Tu Estrategia Actual</h2>
<form id="step2Form">
<p class="radio-group-label mb-3">¿Dónde tienes tus ahorros principales actualmente?</p>
<div id="currentInvestmentOptionsContainer">
<!-- Radio options will be injected here -->
</div>
<div class="step-navigation">
<button type="button" id="prevToStep1" class="btn-secondary"><i class="fas fa-arrow-left mr-2"></i>Anterior</button>
<button type="submit" class="btn-primary">Ver Proyección Inicial <i class="fas fa-chart-line ml-2"></i></button>
</div>
</form>
</div>
<!-- Step 3: Initial Projection -->
<div id="step3" class="step-card">
<h2 class="step-title"><i class="fas fa-bullseye mr-2"></i>Paso 3: Proyección Inicial</h2>
<div id="initialProjectionSummary" class="results-summary-card !mt-0 !shadow-none !p-0">
<!-- Summary will be injected here -->
</div>
<div class="chart-card !mt-4">
<h3 class="section-title !text-lg !mb-3" id="initialChartTitle">Evolución con tu Estrategia Actual</h3>
<div id="chartContainerInitial">
<div id="chartPlaceholderInitial" class="absolute inset-0 flex flex-col items-center justify-center p-4 text-center">
<i class="fas fa-spinner fa-spin fa-2x text-gray-400 mb-3"></i>
<p class="chart-placeholder-text">Generando gráfico...</p>
</div>
<canvas id="initialRetirementChart"></canvas>
</div>
</div>
<div class="step-navigation">
<button type="button" id="prevToStep2" class="btn-secondary"><i class="fas fa-arrow-left mr-2"></i>Anterior</button>
<button type="button" id="goToStep4" class="btn-primary">Optimizar mis Finanzas <i class="fas fa-rocket ml-2"></i></button>
</div>
</div>
<!-- Step 4: Optimize Savings -->
<div id="step4" class="step-card">
<h2 class="step-title"><i class="fas fa-hand-holding-usd mr-2"></i>Paso 4: Optimiza tus Finanzas</h2>
<form id="step4Form">
<div>
<h3 class="sub-section-title"><i class="fas fa-cut mr-2 text-orange-500"></i>A. Optimiza tus Gastos Diarios</h3>
<p class="text-sm text-gray-600 mb-4">Selecciona cambios en tus hábitos para generar ahorros mensuales. Verás el impacto total al final del período:</p>
<div id="boostersContainer">
<!-- Booster options will be injected here -->
</div>
<div class="booster-option mt-6">
<input type="checkbox" id="booster-all" data-saving="0">
<label for="booster-all" class="booster-label-container cursor-pointer">
<div class="flex items-center">
<i class="fas fa-check-double mr-3 text-lg text-purple-500"></i>
<div>
<span class="booster-name">¡Todas las Anteriores! (Optimizar Gastos)</span>
</div>
</div>
</label>
</div>
<div id="totalBoosterSummaryDisplay" class="total-booster-summary hidden mt-4">
<div>Ahorro Mensual Adicional por Recorte de Gastos: <strong id="totalMonthlyBoosterValue"></strong></div>
<div>Impacto Total por Recorte de Gastos (al final del período): <strong id="totalProjectedBoosterValue"></strong></div>
</div>
</div>
<div class="mt-8">
<h3 class="sub-section-title"><i class="fas fa-donate mr-2 text-indigo-500"></i>B. Define tu Ahorro Mensual Extra</h3>
<p class="text-sm text-gray-600 mb-4">Ingresa una cantidad fija que planeas ahorrar cada mes, además de los cambios en gastos.</p>
<div class="input-group">
<label for="fixedMonthlySaving" class="input-label">Ahorro Mensual Fijo Extra ($):</label>
<input type="number" id="fixedMonthlySaving" class="input-field" placeholder="Ej: 500" min="0" step="any">
</div>
</div>
<div class="mt-8">
<h3 class="sub-section-title"><i class="fas fa-chart-pie mr-2 text-teal-500"></i>C. Optimiza tu Estrategia de Inversión Futura</h3>
<p class="text-sm text-gray-600 mb-4">Elige dónde se invertirán tus ahorros actuales y todos los nuevos ahorros mensuales. Ve el impacto al instante:</p>
<div id="futureInvestmentStrategyContainer">
<!-- Radio options for future strategy will be injected here -->
</div>
<p id="futureStrategyMessage" class="text-sm text-yellow-600 mt-2"></p>
</div>
<div class="step-navigation">
<button type="button" id="prevToStep3" class="btn-secondary"><i class="fas fa-arrow-left mr-2"></i>Anterior</button>
<button type="submit" class="btn-primary">Ver Impacto Final <i class="fas fa-gifts ml-2"></i></button>
</div>
</form>
</div>
<!-- Step 5: Final Projection with Boosters -->
<div id="step5" class="step-card">
<h2 class="step-title"><i class="fas fa-star mr-2"></i>Paso 5: ¡Tu Futuro Mejorado!</h2>
<div id="finalProjectionSummary" class="results-summary-card !mt-0 !shadow-none !p-0">
<!-- Summary will be injected here with new structure -->
</div>
<div class="chart-card !mt-4">
<h3 class="section-title !text-lg !mb-3" id="finalChartTitle">Comparativa de Proyecciones</h3>
<div id="chartContainerFinal">
<div id="chartPlaceholderFinal" class="absolute inset-0 flex flex-col items-center justify-center p-4 text-center">
<i class="fas fa-spinner fa-spin fa-2x text-gray-400 mb-3"></i>
<p class="chart-placeholder-text">Generando gráfico comparativo...</p>
</div>
<canvas id="finalRetirementChart"></canvas>
</div>
</div>
<div class="step-navigation">
<button type="button" id="prevToStep4" class="btn-secondary"><i class="fas fa-arrow-left mr-2"></i>Anterior</button>
<button type="button" id="generatePdfButton" class="btn-success"><i class="fas fa-file-pdf mr-2"></i>Generar PDF</button>
<button type="button" id="startOver" class="btn-primary"><i class="fas fa-redo mr-2"></i>Comenzar de Nuevo</button>
</div>
</div>
</div>
</div>
<!-- Modal for error messages -->
<div id="errorModal" class="modal">
<div class="modal-content">
<div class="text-red-500 text-4xl mb-4"><i class="fas fa-exclamation-triangle"></i></div>
<h2 class="modal-title" id="modalTitle">Error en los Datos</h2>
<p class="modal-body" id="modalMessage">Por favor, revisa los campos.</p>
<button id="closeModalButton" class="modal-button">Entendido</button>
</div>
</div>
<script>
// Ensure jsPDF is loaded before use
const { jsPDF } = window.jspdf;
// html2canvas is globally available after script load
// --- DOM Elements ---
const steps = [
document.getElementById('step1'),
document.getElementById('step2'),
document.getElementById('step3'),
document.getElementById('step4'),
document.getElementById('step5')
];
const step1Form = document.getElementById('step1Form');
const step2Form = document.getElementById('step2Form');
const step4Form = document.getElementById('step4Form');
const ageInfoGlobalEl = document.getElementById('ageInfoGlobal');
const initialProjectionSummaryEl = document.getElementById('initialProjectionSummary');
const finalProjectionSummaryEl = document.getElementById('finalProjectionSummary');
const initialChartCanvas = document.getElementById('initialRetirementChart');
const finalChartCanvas = document.getElementById('finalRetirementChart');
const chartPlaceholderInitialEl = document.getElementById('chartPlaceholderInitial');
const chartPlaceholderFinalEl = document.getElementById('chartPlaceholderFinal');
const initialChartTitleEl = document.getElementById('initialChartTitle');
const finalChartTitleEl = document.getElementById('finalChartTitle');
const boostersContainer = document.getElementById('boostersContainer');
const totalBoosterSummaryDisplayEl = document.getElementById('totalBoosterSummaryDisplay');
const totalMonthlyBoosterValueEl = document.getElementById('totalMonthlyBoosterValue');
const totalProjectedBoosterValueEl = document.getElementById('totalProjectedBoosterValue');
const futureInvestmentStrategyContainerEl = document.getElementById('futureInvestmentStrategyContainer');
const futureStrategyMessageEl = document.getElementById('futureStrategyMessage');
const fixedMonthlySavingInput = document.getElementById('fixedMonthlySaving');
const currentInvestmentOptionsContainerEl = document.getElementById('currentInvestmentOptionsContainer');
const generatePdfButton = document.getElementById('generatePdfButton');
const errorModal = document.getElementById('errorModal');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const closeModalButton = document.getElementById('closeModalButton');
// --- App State ---
let currentStep = 0;
let appData = {
fullName: '', email: '', dob: null, retirementAge: null, currentSavings: null, currentAge: null, investmentDuration: null,
initialSelectedScenario: null,
futureOptimizedScenario: null,
selectedBoostersMonthlySaving: 0,
fixedMonthlySaving: 0,
totalMonthlyAdditionalSaving: 0,
selectedBoosterDetails: [],
initialProjectionData: {}, // Scenario 1: Base
boostersOnlyProjectionData: {}, // Scenario 2: Base + Lifestyle Boosters (using initial strategy)
boostersAndFixedSavingProjectionData: {}, // Scenario 3: Base + Lifestyle + Fixed (using initial strategy)
finalOptimizedProjectionData: {} // Scenario 4: Base + Lifestyle + Fixed (using future strategy)
};
let initialChartInstance, finalChartInstance;
// --- Data Definitions ---
const investmentScenarios = [
{ id: 'caja_fuerte', name: 'Guardados en la caja fuerte', displayNameFull: 'Guardados en la caja fuerte', rate: 0.00, color: '#A0AEC0', icon: 'fa-shield-alt', description: 'Sin crecimiento. El dinero puede perder valor con el tiempo debido a la inflación.' },
{ id: 'banco_tradicional', name: 'Cuenta de Ahorro en Banco Tradicional', displayNameFull: 'Cuenta de Ahorro en Banco Tradicional (Ej: Bank of America, Wells Fargo, CITIBANK, etc.)', rate: 0.0001, color: '#63B3ED', icon: 'fa-university', description: 'Crecimiento mínimo. Generalmente seguro.' },
{ id: 'neobanco', name: 'Cuenta de Ahorro en Neo Banco', displayNameFull: 'Cuenta de Ahorro en Neo Banco', rate: 0.04, color: '#48BB78', icon: 'fa-mobile-alt', description: 'Crecimiento moderado. Opciones digitales.' },
{ id: 'bolsa_valores', name: 'Inversión en la Bolsa de Valores', displayNameFull: 'Inversión en la Bolsa de Valores', rate: 0.10, color: '#F59E0B', icon: 'fa-chart-line', description: 'Potencial de alto crecimiento y riesgo.' }
];
const lifestyleBoosters = [
{ id: 'cafe', name: 'Ahorrar café diario', monthlySaving: 180, icon: 'fa-coffee' },
{ id: 'cenas', name: 'Ahorrar cenas de fin de semana', monthlySaving: 480, icon: 'fa-utensils' },
{ id: 'leasing', name: 'Eliminar leasing de auto', monthlySaving: 750, icon: 'fa-car' }
];
// --- UI Navigation & Modal Functions ---
function navigateToStep(stepIndex) {
steps.forEach((step, index) => step.classList.toggle('active', index === stepIndex));
currentStep = stepIndex;
window.scrollTo(0,0);
if (stepIndex === 3) {
renderFutureInvestmentStrategyOptions();
}
}
document.getElementById('prevToStep1').addEventListener('click', () => navigateToStep(0));
document.getElementById('prevToStep2').addEventListener('click', () => navigateToStep(1));
document.getElementById('prevToStep3').addEventListener('click', () => navigateToStep(2));
document.getElementById('prevToStep4').addEventListener('click', () => navigateToStep(3));
document.getElementById('goToStep4').addEventListener('click', () => navigateToStep(3));
document.getElementById('startOver').addEventListener('click', () => { resetFullApp(); navigateToStep(0); });
generatePdfButton.addEventListener('click', generatePDF);
function showModal(title, message) {
modalTitle.textContent = title;
modalMessage.textContent = message;
errorModal.style.display = 'flex';
}
closeModalButton.addEventListener('click', () => errorModal.style.display = 'none');
window.addEventListener('click', (event) => { if (event.target === errorModal) errorModal.style.display = 'none'; });
// --- Dynamic Content Rendering ---
function renderCurrentInvestmentOptions() {
currentInvestmentOptionsContainerEl.innerHTML = '';
investmentScenarios.forEach(scenario => {
const div = document.createElement('div');
div.className = 'radio-option';
div.innerHTML = `
<input type="radio" id="scenario-${scenario.id}" name="currentInvestmentScenario" value="${scenario.id}" required>
<label for="scenario-${scenario.id}" class="radio-label-text flex-grow flex items-center cursor-pointer">
<i class="fas ${scenario.icon} mr-3 text-lg" style="color:${scenario.color};"></i>
<span>${scenario.displayNameFull} (${(scenario.rate * 100).toFixed(2)}%)</span>
</label>
`;
currentInvestmentOptionsContainerEl.appendChild(div);
});
}
function renderFutureInvestmentStrategyOptions() {
futureInvestmentStrategyContainerEl.innerHTML = '';
let firstEnabledOptionId = null;
let countEnabledOptions = 0;
investmentScenarios.forEach(scenario => {
const div = document.createElement('div');
div.className = 'radio-option justify-between';
let isDisabled = false;
if (appData.initialSelectedScenario) {
isDisabled = scenario.rate < appData.initialSelectedScenario.rate ||
(scenario.rate === appData.initialSelectedScenario.rate && scenario.id !== appData.initialSelectedScenario.id);
}
if (!isDisabled) {
countEnabledOptions++;
if (!firstEnabledOptionId) {
firstEnabledOptionId = `future-scenario-${scenario.id}`;
}
}
if (appData.initialSelectedScenario && scenario.id === appData.initialSelectedScenario.id) {
isDisabled = false;
}
div.innerHTML = `
<label for="future-scenario-${scenario.id}" class="radio-label-text flex items-center cursor-pointer ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}">
<input type="radio" id="future-scenario-${scenario.id}" name="futureInvestmentScenario" value="${scenario.id}" ${isDisabled ? 'disabled' : ''} required class="mr-3">
<i class="fas ${scenario.icon} mr-2 text-lg" style="color:${scenario.color};"></i>
<span>${scenario.name} (${(scenario.rate * 100).toFixed(2)}%)</span>
</label>
<span id="preview-future-${scenario.id}" class="future-strategy-preview ${isDisabled ? 'opacity-50' : ''}"></span>
`;
futureInvestmentStrategyContainerEl.appendChild(div);
if (!isDisabled) {
document.getElementById(`future-scenario-${scenario.id}`).addEventListener('change', updateFutureStrategyPreview);
}
});
const initialFutureRadio = appData.initialSelectedScenario ? document.getElementById(`future-scenario-${appData.initialSelectedScenario.id}`) : null;
if (firstEnabledOptionId && (!initialFutureRadio || initialFutureRadio.disabled)) {
document.getElementById(firstEnabledOptionId).checked = true;
} else if (initialFutureRadio) {
initialFutureRadio.checked = true;
}
if (countEnabledOptions <= 1 && appData.initialSelectedScenario) {
futureStrategyMessageEl.textContent = "Tu estrategia actual ya es una de las mejores opciones. Considera aumentar tus ahorros para un mayor impacto.";
} else {
futureStrategyMessageEl.textContent = "";
}
updateFutureStrategyPreview();
}
function updateFutureStrategyPreview() {
if (!appData.currentSavings || !appData.investmentDuration) return;
appData.fixedMonthlySaving = parseFloat(fixedMonthlySavingInput.value) || 0;
appData.totalMonthlyAdditionalSaving = appData.selectedBoostersMonthlySaving + appData.fixedMonthlySaving;
const allFutureRadios = document.querySelectorAll('input[name="futureInvestmentScenario"]');
allFutureRadios.forEach(radio => {
const scenarioId = radio.value;
const previewEl = document.getElementById(`preview-future-${scenarioId}`);
if (!previewEl) return;
const scenarioForPreview = investmentScenarios.find(s => s.id === scenarioId);
if (scenarioForPreview && !radio.disabled) {
const annualTotalAdditionalSaving = appData.totalMonthlyAdditionalSaving * 12;
const projectionPreview = calculateProjection(
appData.currentSavings,
appData.investmentDuration,
scenarioForPreview.rate,
annualTotalAdditionalSaving
);
previewEl.textContent = `➔ $${projectionPreview.finalAmount.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
} else {
previewEl.textContent = '';
}
});
}
function renderBoosters() {
boostersContainer.innerHTML = '';
lifestyleBoosters.forEach(booster => {
const boosterDiv = document.createElement('div');
boosterDiv.className = 'booster-option';
boosterDiv.innerHTML = `
<div class="booster-label-container">
<input type="checkbox" id="booster-${booster.id}" data-saving="${booster.monthlySaving}" data-name="${booster.name}" class="lifestyle-booster-checkbox">
<label for="booster-${booster.id}" class="booster-label cursor-pointer">
<div class="flex items-center">
<i class="fas ${booster.icon} mr-3 text-lg" style="color: ${investmentScenarios[2].color};"></i>
<div>
<span class="booster-name">${booster.name}</span>
<div class="booster-saving">Ahorro mensual: $${booster.monthlySaving.toLocaleString('es-ES', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</div>
</div>
</div>
</label>
</div>
<span id="impact-booster-${booster.id}" class="booster-final-impact"></span>
`;
boostersContainer.appendChild(boosterDiv);
document.getElementById(`booster-${booster.id}`).addEventListener('change', () => {
updateTotalBoosterSavings();
updateFutureStrategyPreview();
});
});
document.getElementById('booster-all').addEventListener('change', (event) => {
handleSelectAllBoosters(event);
updateFutureStrategyPreview();
});
updateBoosterImpactPreview();
}
function updateBoosterImpactPreview() {
if (!appData.currentSavings || !appData.investmentDuration || !appData.initialSelectedScenario || !appData.initialProjectionData.finalAmount) return;
lifestyleBoosters.forEach(booster => {
const impactEl = document.getElementById(`impact-booster-${booster.id}`);
if (!impactEl) return;
const isChecked = document.getElementById(`booster-${booster.id}`).checked;
if (isChecked) {
const projectionWithThisBoosterOnly = calculateProjection(
appData.currentSavings,
appData.investmentDuration,
appData.initialSelectedScenario.rate,
booster.monthlySaving * 12
);
impactEl.textContent = `➔ Final: $${projectionWithThisBoosterOnly.finalAmount.toLocaleString('es-ES', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
} else {
impactEl.textContent = '';
}
});
}
function handleSelectAllBoosters(event) {
const isChecked = event.target.checked;
lifestyleBoosters.forEach(booster => { document.getElementById(`booster-${booster.id}`).checked = isChecked; });
updateTotalBoosterSavings();
}
function updateTotalBoosterSavings() {
let totalMonthlyBoost = 0; let allChecked = true; appData.selectedBoosterDetails = [];
lifestyleBoosters.forEach(booster => {
const checkbox = document.getElementById(`booster-${booster.id}`);
if (checkbox.checked) {
totalMonthlyBoost += parseFloat(checkbox.dataset.saving);
appData.selectedBoosterDetails.push(checkbox.dataset.name);
} else { allChecked = false; }
});
document.getElementById('booster-all').checked = allChecked;
appData.selectedBoostersMonthlySaving = totalMonthlyBoost;
if (totalMonthlyBoost > 0) {
totalMonthlyBoosterValueEl.textContent = `$${appData.selectedBoostersMonthlySaving.toLocaleString('es-ES', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
if(appData.initialSelectedScenario && appData.investmentDuration) {
const fvOfBoostersOnly = calculateProjection(0, appData.investmentDuration, appData.initialSelectedScenario.rate, appData.selectedBoostersMonthlySaving * 12);
totalProjectedBoosterValueEl.textContent = `$${fvOfBoostersOnly.finalAmount.toLocaleString('es-ES', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
} else {
totalProjectedBoosterValueEl.textContent = '$0.00';
}
totalBoosterSummaryDisplayEl.classList.remove('hidden');
} else {
totalBoosterSummaryDisplayEl.classList.add('hidden');
}
updateBoosterImpactPreview();
}
// --- Calculation Logic ---
function calculateProjection(initialSavings, durationYears, annualRate, annualContribution = 0) {
let balance = initialSavings; const yearlyData = [balance]; let totalContributions = 0;
for (let year = 1; year <= durationYears; year++) {
balance += annualContribution; totalContributions += annualContribution;
balance *= (1 + annualRate); yearlyData.push(parseFloat(balance.toFixed(2)));
}
const totalInterest = balance - initialSavings - totalContributions;
return { finalAmount: balance, totalInterest, yearlyData, totalContributions };
}
// --- Step Submissions & Display Updates ---
step1Form.addEventListener('submit', function(event) {
event.preventDefault();
appData.fullName = document.getElementById('fullName').value;
appData.email = document.getElementById('email').value;
appData.dob = document.getElementById('dob').value;
appData.retirementAge = parseInt(document.getElementById('retirementAge').value);
appData.currentSavings = parseFloat(document.getElementById('currentSavings').value);
if (!appData.fullName || !appData.email || !appData.dob || isNaN(appData.retirementAge) || isNaN(appData.currentSavings)) {
showModal('Datos Incompletos', 'Por favor, completa todos los campos del Paso 1.'); return;
}
if (!/^\S+@\S+\.\S+$/.test(appData.email)) { showModal('Correo Inválido', 'Por favor, ingresa un correo electrónico válido.'); return; }
const birthDate = new Date(appData.dob); const today = new Date();
if (birthDate >= today) { showModal('Fecha de Nacimiento Inválida', 'La fecha de nacimiento no puede ser hoy o futura.'); return; }
appData.currentAge = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) appData.currentAge--;
if (appData.retirementAge <= appData.currentAge) { showModal('Edad de Retiro Inválida', `La edad de retiro (${appData.retirementAge}) debe ser mayor que tu edad actual (${appData.currentAge}).`); return; }
if (appData.currentSavings < 0) { showModal('Ahorros Inválidos', 'Tus ahorros actuales no pueden ser negativos.'); return; }
appData.investmentDuration = appData.retirementAge - appData.currentAge;
ageInfoGlobalEl.innerHTML = `<i class="fas fa-user-circle mr-2 text-purple-500"></i>Hola ${appData.fullName.split(' ')[0]}! <i class="fas fa-birthday-cake ml-3 mr-2 text-blue-500"></i>Edad actual: <strong>${appData.currentAge}</strong>. <i class="fas fa-hourglass-half ml-3 mr-2 text-green-500"></i>Ahorrarás por <strong>${appData.investmentDuration} años</strong>.`;
navigateToStep(1);
});
step2Form.addEventListener('submit', function(event) {
event.preventDefault();
const selectedScenarioIdRadio = document.querySelector('input[name="currentInvestmentScenario"]:checked');
if (!selectedScenarioIdRadio) { showModal('Selección Requerida', 'Por favor, selecciona dónde tienes tus ahorros actualmente.'); return; }
appData.initialSelectedScenario = investmentScenarios.find(s => s.id === selectedScenarioIdRadio.value);
appData.initialProjectionData = calculateProjection(appData.currentSavings, appData.investmentDuration, appData.initialSelectedScenario.rate);
displayInitialProjection();
navigateToStep(2);
updateTotalBoosterSavings();
});
function displayInitialProjection() {
const { finalAmount, totalInterest, yearlyData } = appData.initialProjectionData;
const scenario = appData.initialSelectedScenario;
initialProjectionSummaryEl.innerHTML = `
<div class="summary-section !border-none">
<div class="scenario-header mb-3">
<i class="fas ${scenario.icon} scenario-icon" style="color:${scenario.color};"></i>
<p class="summary-title !mb-0">${scenario.name}</p>
</div>
<p class="text-sm text-gray-600 mb-4">${scenario.description}</p>
<div class="summary-item"><span>Ahorros Iniciales:</span> <strong>$${appData.currentSavings.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}</strong></div>
<div class="summary-item"><span>Intereses Ganados:</span> <strong>$${totalInterest.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})} (en ${appData.investmentDuration} años)</strong></div>
<div class="summary-item summary-total-final !text-lg !mt-3"><span>Monto Final Estimado:</span> <strong style="color:${scenario.color};">$${finalAmount.toLocaleString('es-ES', {minimumFractionDigits: 2, maximumFractionDigits: 2})}</strong></div>
</div>`;
const chartLabels = ['Inicio']; for (let year=1;year<=appData.investmentDuration;year++) chartLabels.push(`Año ${year}`);
initialChartTitleEl.textContent = `Evolución con: ${scenario.name}`;
updateChart(initialChartInstance, initialChartCanvas, chartPlaceholderInitialEl, chartLabels, [{label:scenario.name,data:yearlyData,borderColor:scenario.color,backgroundColor:scenario.color.replace(')','.2)').replace('rgb','rgba'),fill:true,tension:0.2}],appData.investmentDuration,'initialChartInstance');
}
fixedMonthlySavingInput.addEventListener('input', updateFutureStrategyPreview);
step4Form.addEventListener('submit', function(event) {
event.preventDefault();
appData.fixedMonthlySaving = parseFloat(fixedMonthlySavingInput.value) || 0;
if (appData.fixedMonthlySaving < 0) {
showModal('Ahorro Mensual Inválido', 'El ahorro mensual fijo no puede ser negativo.');
return;
}
appData.totalMonthlyAdditionalSaving = appData.selectedBoostersMonthlySaving + appData.fixedMonthlySaving;
const annualTotalAdditionalSaving = appData.totalMonthlyAdditionalSaving * 12;
const selectedFutureScenarioIdRadio = document.querySelector('input[name="futureInvestmentScenario"]:checked');
if (!selectedFutureScenarioIdRadio) { showModal('Selección Requerida', 'Por favor, selecciona tu estrategia de inversión futura en la sección C.'); return; }
appData.futureOptimizedScenario = investmentScenarios.find(s => s.id === selectedFutureScenarioIdRadio.value);
appData.boostersOnlyProjectionData = calculateProjection(appData.currentSavings, appData.investmentDuration, appData.initialSelectedScenario.rate, appData.selectedBoostersMonthlySaving * 12);
appData.boostersAndFixedSavingProjectionData = calculateProjection(appData.currentSavings, appData.investmentDuration, appData.initialSelectedScenario.rate, annualTotalAdditionalSaving);
appData.finalOptimizedProjectionData = calculateProjection(appData.currentSavings, appData.investmentDuration, appData.futureOptimizedScenario.rate, annualTotalAdditionalSaving);
displayFinalProjection();
navigateToStep(4);
});
function displayFinalProjection() {
const scenarioInitial = appData.initialSelectedScenario;
const scenarioFuture = appData.futureOptimizedScenario;
const projS1 = appData.initialProjectionData;
const projS2 = appData.boostersOnlyProjectionData;
const projS3 = appData.boostersAndFixedSavingProjectionData;
const projS4 = appData.finalOptimizedProjectionData;
let summaryHtml = `<div class="summary-section">
<p class="summary-title">Escenario 1: Tu Situación Actual (Sin Cambios)</p>
<div class="summary-item"><span>Estrategia:</span> <strong>${scenarioInitial.name}</strong></div>
<div class="summary-item"><span>Monto Final Estimado:</span> <strong>$${projS1.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}</strong></div>
</div>`;
summaryHtml += `<div class="summary-section">
<p class="summary-title">Escenario 2: Con Recorte de Gastos</p>
<div class="summary-item"><span>Estrategia (Inicial):</span> <strong>${scenarioInitial.name}</strong></div>`;
lifestyleBoosters.forEach(booster => {
const isSelected = appData.selectedBoosterDetails.includes(booster.name);
const totalBoosterContribution = isSelected ? (booster.monthlySaving * 12 * appData.investmentDuration) : 0;
summaryHtml += `<div class="summary-item pl-4"><span>+ ${booster.name} (Total Aportado):</span> <span class="${isSelected ? 'text-green-600' : 'text-gray-500'}">+$${totalBoosterContribution.toLocaleString('es-ES',{minimumFractionDigits: 0, maximumFractionDigits: 0})}</span></div>`;
});
if (!appData.selectedBoostersMonthlySaving > 0 && lifestyleBoosters.length > 0) { // Check if there were boosters to select from
summaryHtml += `<p class="text-sm text-gray-500 pl-4 mt-2">No se seleccionaron recortes de gastos.</p>`;
}
summaryHtml += `<div class="summary-item summary-total"><span>Subtotal Estimado (Escenario 2):</span> <strong>$${projS2.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}</strong></div>
</div>`;
summaryHtml += `<div class="summary-section">
<p class="summary-title">Escenario 3: Con Recorte de Gastos + Ahorro Mensual Extra</p>
<div class="summary-item"><span>Estrategia (Inicial):</span> <strong>${scenarioInitial.name}</strong></div>`;
// Display booster contributions (even if zero)
if (appData.selectedBoostersMonthlySaving > 0) {
summaryHtml += `<div class="summary-item pl-4"><span>Aportes por Recorte de Gastos:</span> <span class="text-green-600">+$${(appData.selectedBoostersMonthlySaving * 12 * appData.investmentDuration).toLocaleString('es-ES',{minimumFractionDigits: 0, maximumFractionDigits: 0})}</span></div>`;
} else {
summaryHtml += `<div class="summary-item pl-4"><span>Aportes por Recorte de Gastos:</span> <span class="text-gray-500">+$0.00</span></div>`;
}
// Display fixed monthly saving contribution (even if zero)
const fixedSavingContribution = appData.fixedMonthlySaving * 12 * appData.investmentDuration;
summaryHtml += `<div class="summary-item pl-4"><span>Aportes por Ahorro Mensual Extra:</span> <span class="${appData.fixedMonthlySaving > 0 ? 'text-green-600' : 'text-gray-500'}">+$${fixedSavingContribution.toLocaleString('es-ES',{minimumFractionDigits: 0, maximumFractionDigits: 0})}</span></div>`;
summaryHtml += `<div class="summary-item summary-total"><span>Subtotal Estimado (Escenario 3):</span> <strong>$${projS3.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}</strong></div>
</div>`;
summaryHtml += `<div class="summary-section">
<p class="summary-title">Escenario 4: Con Recorte de Gastos + Ahorro Mensual Extra + Nueva Estrategia de Inversión</p>
<div class="strategy-change">
<span>${scenarioInitial.name}</span>
<i class="fas fa-arrow-right"></i>
<span>${scenarioFuture.name}</span>
</div>`;
if (appData.totalMonthlyAdditionalSaving > 0) {
summaryHtml += `<div class="summary-item pl-4"><span>Aportes Totales Extra (Recorte + Fijo):</span> <span class="text-green-600">+$${(appData.totalMonthlyAdditionalSaving * 12 * appData.investmentDuration).toLocaleString('es-ES',{minimumFractionDigits: 0, maximumFractionDigits: 0})}</span></div>`;
} else {
summaryHtml += `<p class="text-sm text-gray-500 pl-4">Sin ahorros adicionales definidos.</p>`;
}
summaryHtml += `<div class="summary-item summary-total-final"><span>¡Monto Final Proyectado (Escenario 4)!:</span> <strong class="text-xl">$${projS4.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}</strong></div>
</div>`;
summaryHtml += `<div class="mt-6 p-4 bg-green-100 border border-green-300 rounded-md text-center">
<p class="text-green-800 font-bold text-lg">
¡Impacto Total de Optimización (Escenario 4 vs Escenario 1):
<strong class="text-2xl">$${(projS4.finalAmount - projS1.finalAmount).toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}</strong> más para tu retiro!
</p>
</div>`;
finalProjectionSummaryEl.innerHTML = summaryHtml;
const chartLabels = ['Inicio']; for (let year=1;year<=appData.investmentDuration;year++) chartLabels.push(`Año ${year}`);
const datasetsForFinalChart = [
{ label: `Esc. 1: ${scenarioInitial.name} (Actual)`, data: projS1.yearlyData, borderColor: '#CBD5E0', backgroundColor:'rgba(203,213,224,0.1)', fill:false, tension:0.2, borderDash:[5,5] }
];
if (projS2.finalAmount.toFixed(2) !== projS1.finalAmount.toFixed(2)) {
datasetsForFinalChart.push({ label: `Esc. 2: + Recorte Gastos (${scenarioInitial.name})`, data: projS2.yearlyData, borderColor: '#A78BFA', backgroundColor: 'rgba(167,139,250,0.1)', fill:true, tension:0.2, borderDash:[4,4] });
}
if (projS3.finalAmount.toFixed(2) !== projS2.finalAmount.toFixed(2) || (datasetsForFinalChart.length === 1 && projS3.finalAmount.toFixed(2) !== projS1.finalAmount.toFixed(2))) {
datasetsForFinalChart.push({ label: `Esc. 3: + Ahorro Extra (${scenarioInitial.name})`, data: projS3.yearlyData, borderColor: scenarioInitial.color, backgroundColor: scenarioInitial.color.replace(')','.15)').replace('rgb','rgba'), fill:true, tension:0.2, borderDash:[2,2] });
}
if (projS4.finalAmount.toFixed(2) !== projS3.finalAmount.toFixed(2) || (datasetsForFinalChart.length === 1 && projS4.finalAmount.toFixed(2) !== projS1.finalAmount.toFixed(2))) {
datasetsForFinalChart.push({ label: `Esc. 4: + Nueva Estrategia (${scenarioFuture.name})`, data: projS4.yearlyData, borderColor: scenarioFuture.color, backgroundColor: scenarioFuture.color.replace(')','.2)').replace('rgb','rgba'), fill:true, tension:0.2, borderWidth: 2.5 });
}
finalChartTitleEl.textContent = `Comparativa de Escenarios de Retiro`;
updateChart(finalChartInstance, finalChartCanvas, chartPlaceholderFinalEl, chartLabels, datasetsForFinalChart, appData.investmentDuration, 'finalChartInstance');
}
function updateChart(chartInstanceVar, canvasElement, placeholderElement, labels, datasets, duration, instanceName) {
canvasElement.style.display = 'block'; placeholderElement.style.display = 'none';
if (instanceName === 'initialChartInstance' && initialChartInstance) initialChartInstance.destroy();
if (instanceName === 'finalChartInstance' && finalChartInstance) finalChartInstance.destroy();
const ctx = canvasElement.getContext('2d');
const newChartInstance = new Chart(ctx, {
type:'line', data:{labels:labels,datasets:datasets},
options:{ responsive:true,maintainAspectRatio:false,scales:{ y:{beginAtZero:true,ticks:{callback:v=>'$'+v.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2}),color:'#4a5568',font:{size:11}},title:{display:true,text:'Monto Acumulado ($)',color:'#2d3748',font:{weight:'600'}},grid:{color:'#e2e8f0'}},x:{ticks:{autoSkip:true,maxTicksLimit:duration>15?(duration>30?8:10):duration+1,color:'#4a5568',font:{size:11}},title:{display:true,text:'Años de Inversión',color:'#2d3748',font:{weight:'600'}},grid:{display:false}}},plugins:{legend:{position:'bottom',labels:{usePointStyle:true,padding:25,boxWidth:10,color:'#2d3748',font:{size:12}}},tooltip:{mode:'index',intersect:false,backgroundColor:'rgba(0,0,0,0.75)',titleFont:{weight:'bold',size:14},bodyFont:{size:12},padding:12,cornerRadius:6,callbacks:{label:c=>(c.dataset.label||'')+': $'+c.parsed.y.toLocaleString('es-ES',{minimumFractionDigits:2,maximumFractionDigits:2})}}},interaction:{mode:'nearest',axis:'x',intersect:false}}
});
if(instanceName==='initialChartInstance')initialChartInstance=newChartInstance; if(instanceName==='finalChartInstance')finalChartInstance=newChartInstance;
}
async function generatePDF() {
const pdfButton = document.getElementById('generatePdfButton');
pdfButton.disabled = true; pdfButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Generando...';
const doc = new jsPDF({orientation:'p',unit:'mm',format:'a4'}); const today = new Date(); const formattedDate = `${today.getDate()}/${today.getMonth()+1}/${today.getFullYear()}`; let yPos = 20; const margin = 15; const pageWidth = doc.internal.pageSize.getWidth(); const contentWidth = pageWidth-(2*margin);
function addTextToDocRobust(doc, text, x, y, options = {}, pageContentWidth, pageMargin) {
const pageHeight = doc.internal.pageSize.getHeight();
const fontSize = options.fontSize || doc.getFontSize();
let lineHeightFactor = options.lineHeightFactor || 1.35;
if (fontSize < 10) lineHeightFactor = 1.45;
const lineSpacing = (fontSize / doc.internal.scaleFactor) * lineHeightFactor;
doc.setFontSize(fontSize);
if (options.fontStyle) doc.setFont(undefined, options.fontStyle);
if (options.textColor) doc.setTextColor(options.textColor[0], options.textColor[1], options.textColor[2]);
const textLines = doc.splitTextToSize(text, options.maxWidth || pageContentWidth);
textLines.forEach((line, index) => {
if (y + lineSpacing > pageHeight - pageMargin && index < textLines.length -1) {
doc.addPage();
y = pageMargin;
}
doc.text(line, x, y, { align: options.align || 'left' });
y += lineSpacing;
});
if (options.fontStyle) doc.setFont(undefined, 'normal');
if (options.textColor) doc.setTextColor(74, 85, 104);
return y;
}
function addSectionTitleToPdf(text) {
yPos = addTextToDocRobust(doc, text, margin, yPos, { fontSize: 14, textColor: [45, 55, 72], fontStyle:'bold' }, contentWidth, margin);
doc.setDrawColor(74, 144, 226); doc.setLineWidth(0.5);
doc.line(margin, yPos - 4, margin + 50, yPos - 4);
yPos += 3;
}
function addContentPairToPdf(label, value, valueOptions = {}) {
const defaultYGap = 3;
const labelMaxWidth = 70;
const valueXOffset = margin + labelMaxWidth -15;
doc.setFontSize(11);
doc.setTextColor(74, 85, 104);
doc.setFont(undefined, 'bold');
let yAfterLabel = addTextToDocRobust(doc, label, margin, yPos, { fontSize: 11, maxWidth: labelMaxWidth, fontStyle: 'bold', textColor: [74,85,104] }, contentWidth, margin);
doc.setFontSize(valueOptions.fontSize || 11);
doc.setTextColor(valueOptions.color ? valueOptions.color[0] : 74,
valueOptions.color ? valueOptions.color[1] : 85,
valueOptions.color ? valueOptions.color[2] : 104);
doc.setFont(undefined, valueOptions.fontStyle || 'normal');
let yAfterValue = addTextToDocRobust(doc, value, valueXOffset, yPos, { fontSize: valueOptions.fontSize || 11, maxWidth: contentWidth - valueXOffset + margin, fontStyle: valueOptions.fontStyle || 'normal', textColor: valueOptions.color }, contentWidth, margin);
yPos = Math.max(yAfterLabel, yAfterValue) + defaultYGap;
}
function addDetailListItemToPdf(text, xIndent = 5) {
yPos = addTextToDocRobust(doc, text, margin + xIndent, yPos, { fontSize: 10, textColor: [100,116,139], maxWidth: contentWidth - xIndent - 5, lineHeightFactor: 1.4 }, contentWidth, margin);
}
// --- PDF Content Generation ---
doc.setFontSize(20);doc.setTextColor(44,82,130);yPos=addTextToDocRobust(doc, "Informe de Proyección de Retiro", pageWidth/2, yPos, {align:"center",fontSize:20, textColor:[44,82,130]}, contentWidth, margin);
doc.setFontSize(10);doc.setTextColor(100,116,139);yPos=addTextToDocRobust(doc, `Generado el: ${formattedDate}`, pageWidth/2, yPos, {align:"center",fontSize:10, textColor:[100,116,139]}, contentWidth, margin);
yPos+=5;doc.setDrawColor(226,232,240);doc.line(margin,yPos,pageWidth-margin,yPos);yPos+=8;
addSectionTitleToPdf("Datos Personales");
addContentPairToPdf("Nombre:",appData.fullName);
addContentPairToPdf("Correo:",appData.email);
yPos+=3;
addSectionTitleToPdf("Configuración de Proyección");
addContentPairToPdf("Fecha de Nacimiento:",new Date(appData.dob).toLocaleDateString('es-ES'));
addContentPairToPdf("Edad Actual:",`${appData.currentAge} años`);
addContentPairToPdf("Edad de Retiro:",`${appData.retirementAge} años`);
addContentPairToPdf("Años para Retiro:",`${appData.investmentDuration} años`);
addContentPairToPdf("Ahorros Actuales:",`$${appData.currentSavings.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}`);
yPos+=3;
addSectionTitleToPdf("Escenario 1: Tu Situación Actual (Sin Cambios)");
addContentPairToPdf("Estrategia:",`${appData.initialSelectedScenario.name} (${(appData.initialSelectedScenario.rate*100).toFixed(2)}%)`);
addContentPairToPdf("Monto Final Estimado:",`$${appData.initialProjectionData.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}`);
yPos+=3;
if(appData.selectedBoostersMonthlySaving > 0 || appData.fixedMonthlySaving > 0){
addSectionTitleToPdf("Escenario 2: Con Recorte de Gastos");
addContentPairToPdf("Estrategia:", `${appData.initialSelectedScenario.name}`);
if (appData.selectedBoostersMonthlySaving > 0) {
yPos = addTextToDocRobust(doc, "Detalle Recorte de Gastos:", margin, yPos, { fontSize: 11, fontStyle: 'bold', textColor: [74,85,104] }, contentWidth, margin);
appData.selectedBoosterDetails.forEach(detail => {
const boosterObj = lifestyleBoosters.find(b => b.name === detail);
if (boosterObj) {
const totalBoosterSaving = boosterObj.monthlySaving * 12 * appData.investmentDuration;
addDetailListItemToPdf(`+ ${detail} (Total Aportado): +$${totalBoosterSaving.toLocaleString('es-ES', {minimumFractionDigits: 0, maximumFractionDigits: 0})}`);
}
});
yPos += 1;
} else {
addDetailListItemToPdf("Sin Recorte de Gastos seleccionados."); yPos +=1;
}
addContentPairToPdf("Monto Final Estimado (Esc. 2):",`$${appData.boostersOnlyProjectionData.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}`);
yPos+=3;
addSectionTitleToPdf("Escenario 3: Con Recorte de Gastos + Ahorro Mensual Extra");
addContentPairToPdf("Estrategia:", `${appData.initialSelectedScenario.name}`);
if (appData.fixedMonthlySaving > 0) {
addDetailListItemToPdf(`+ Ahorro Mensual Extra (Total Aportado): +$${(appData.fixedMonthlySaving * 12 * appData.investmentDuration).toLocaleString('es-ES', {minimumFractionDigits: 0, maximumFractionDigits: 0})}`); yPos +=1;
} else {
addDetailListItemToPdf("Sin Ahorro Mensual Extra definido."); yPos += 1;
}
if (appData.selectedBoostersMonthlySaving > 0 && appData.fixedMonthlySaving > 0) {
addDetailListItemToPdf(`Total Aportes Extra (Recorte + Fijo): +$${((appData.selectedBoostersMonthlySaving + appData.fixedMonthlySaving) * 12 * appData.investmentDuration).toLocaleString('es-ES', {minimumFractionDigits: 0, maximumFractionDigits: 0})}`); yPos +=1;
}
addContentPairToPdf("Monto Final Estimado (Esc. 3):",`$${appData.boostersAndFixedSavingProjectionData.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}`);
yPos+=3;
}
addSectionTitleToPdf("Escenario 4: Con Recorte de Gastos + Ahorro Mensual Extra + Nueva Estrategia de Inversión");
addContentPairToPdf("Estrategia Anterior:", `${appData.initialSelectedScenario.name}`);
addContentPairToPdf("Nueva Estrategia:", `${appData.futureOptimizedScenario.name} (${(appData.futureOptimizedScenario.rate*100).toFixed(2)}%)`);
if(appData.totalMonthlyAdditionalSaving > 0) addContentPairToPdf("Incluye Ahorros Extra:", "Sí"); else addContentPairToPdf("Incluye Ahorros Extra:", "No");
addContentPairToPdf("¡Monto Final Proyectado!:",`$${appData.finalOptimizedProjectionData.finalAmount.toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}`, {fontStyle: 'bold', color: [39,124,83], fontSize: 12});
yPos+=3;
yPos = addTextToDocRobust(doc, `¡Impacto Total (Esc. 4 vs Esc. 1)!: +$${(appData.finalOptimizedProjectionData.finalAmount-appData.initialProjectionData.finalAmount).toLocaleString('es-ES',{minimumFractionDigits: 2, maximumFractionDigits: 2})}`, margin, yPos, {fontSize:13, fontStyle:'bold', textColor:[39,124,83]}, contentWidth, margin);
yPos+=5;
if(finalChartCanvas&&finalChartInstance){
try{
const chartImageContainer=document.getElementById('chartContainerFinal');const placeholderWasVisible=chartPlaceholderFinalEl.style.display!=='none';if(placeholderWasVisible){finalChartCanvas.style.display='block';chartPlaceholderFinalEl.style.display='none';await new Promise(resolve=>setTimeout(resolve,100));}
const canvas=await html2canvas(chartImageContainer,{scale:2,logging:false,useCORS:true,backgroundColor:'#ffffff'});if(placeholderWasVisible){finalChartCanvas.style.display='none';chartPlaceholderFinalEl.style.display='flex';}
const imgData=canvas.toDataURL('image/png');const imgProps=doc.getImageProperties(imgData);const pdfChartWidth=contentWidth-10;const pdfChartHeight=(imgProps.height*pdfChartWidth)/imgProps.width;
const chartTitleHeight = 7;
if(yPos + chartTitleHeight + pdfChartHeight + 10 > doc.internal.pageSize.getHeight() - margin){
doc.addPage();
yPos = margin;
}
addSectionTitleToPdf("Gráfico Comparativo de Escenarios:"); yPos -=2;
doc.addImage(imgData,'PNG',margin+5,yPos,pdfChartWidth,pdfChartHeight);yPos+=pdfChartHeight+10;
}catch(error){console.error("Error PDF chart:",error);if(yPos+10>doc.internal.pageSize.getHeight()-margin){doc.addPage();yPos=margin;}doc.setTextColor(229,62,62);yPos=addTextToDocRobust(doc,"Error: No se pudo generar el gráfico.",margin,yPos, {}, contentWidth, margin);}
}
if(yPos > doc.internal.pageSize.getHeight()-25){doc.addPage();yPos=margin;}doc.setDrawColor(226,232,240);doc.line(margin,yPos,pageWidth-margin,yPos);yPos+=7;doc.setFontSize(9);doc.setTextColor(100,116,139);const footerText="Nota: Proyección basada en datos y tasas estimadas. Resultados reales pueden variar. Consulte a un asesor financiero.";yPos=addTextToDocRobust(doc,footerText,margin,yPos,{fontSize:9,maxWidth:contentWidth}, contentWidth, margin);
doc.save(`Proyeccion_Retiro_${appData.fullName.replace(/\s+/g,'_')}.pdf`);
pdfButton.disabled=false;pdfButton.innerHTML='<i class="fas fa-file-pdf mr-2"></i>Generar PDF';
}
function resetFullApp() {
step1Form.reset(); step2Form.reset(); step4Form.reset();
appData={fullName:'',email:'',dob:null,retirementAge:null,currentSavings:null,currentAge:null,investmentDuration:null,initialSelectedScenario:null,futureOptimizedScenario:null,selectedBoostersMonthlySaving:0,fixedMonthlySaving:0,totalMonthlyAdditionalSaving:0,selectedBoosterDetails:[],initialProjectionData:{},boostersOnlyProjectionData:{}, boostersAndFixedSavingProjectionData:{},finalOptimizedProjectionData:{}};
ageInfoGlobalEl.textContent='';initialProjectionSummaryEl.innerHTML='';finalProjectionSummaryEl.innerHTML='';
if(initialChartInstance)initialChartInstance.destroy();initialChartInstance=null;if(finalChartInstance)finalChartInstance.destroy();finalChartInstance=null;
initialChartCanvas.style.display='none';chartPlaceholderInitialEl.style.display='flex';finalChartCanvas.style.display='none';chartPlaceholderFinalEl.style.display='flex';
if(fixedMonthlySavingInput) fixedMonthlySavingInput.value = '';
lifestyleBoosters.forEach(b=>{if(document.getElementById(`booster-${b.id}`))document.getElementById(`booster-${b.id}`).checked=false;});if(document.getElementById('booster-all'))document.getElementById('booster-all').checked=false;updateTotalBoosterSavings();navigateToStep(0);
}
window.addEventListener('load',()=>{renderCurrentInvestmentOptions();renderBoosters();navigateToStep(0);});
</script>
</body>
</html>