// Core Navigation Functions with Enhanced Accessibility function nextStep() { console.log("Next clicked, current step:", currentStep); if (validateCurrentStep() && currentStep < 5) { currentStep++; showStep(currentStep); updateProgress(); // Announce step change for screen readers announceStepChange(currentStep); // Save progress to localStorage for session persistence saveProgress(); } } function prevStep() { if (currentStep > 1) { currentStep--; showStep(currentStep); updateProgress(); announceStepChange(currentStep); saveProgress(); } } function showStep(step) { // Hide all wizard steps document.querySelectorAll(".wizard-step").forEach(el => { el.style.display = "none"; el.setAttribute("aria-hidden", "true"); }); // Show target step const targetStep = document.querySelector(`.wizard-step[data-step="${step}"]`); if (targetStep) { targetStep.style.display = "block"; targetStep.setAttribute("aria-hidden", "false"); // Focus management for accessibility const firstInput = targetStep.querySelector('input, select, button'); if (firstInput && currentStep > 1) { setTimeout(() => firstInput.focus(), 100); } } // Update step indicators document.querySelectorAll(".step-indicator").forEach(el => { el.classList.remove("active"); el.setAttribute("aria-current", "false"); }); const targetIndicator = document.querySelector(`.step-indicator[data-step="${step}"]`); if (targetIndicator) { targetIndicator.classList.add("active"); targetIndicator.setAttribute("aria-current", "step"); } updateProgress(); // Initialize step-specific functions switch(step) { case 1: updateTimelinePreview(); break; case 2: // Family step initialization break; case 3: updateEconomicSettings(); updateCashFlow(); break; case 4: updateGoalsList(); break; case 5: // Results are handled by generateResults() break; } } function updateProgress() { const progress = (currentStep / 5) * 100; const progressBar = document.getElementById("progress-bar"); if (progressBar) { progressBar.style.width = progress + "%"; progressBar.setAttribute("aria-valuenow", progress); progressBar.setAttribute("aria-valuetext", `Step ${currentStep} of 5, ${Math.round(progress)}% complete`); } } // Enhanced validation with accessibility announcements function validateCurrentStep() { hideError(); let isValid = true; let errorMessage = ""; switch (currentStep) { case 1: const currentAge = parseInt(document.getElementById("current-age").value); const retirementAge = parseInt(document.getElementById("retirement-age").value); if (!currentAge || currentAge < 18 || currentAge > 100) { errorMessage = "Please enter a valid current age between 18 and 100"; isValid = false; } else if (!retirementAge || retirementAge < 50 || retirementAge > 80) { errorMessage = "Please enter a valid retirement age between 50 and 80"; isValid = false; } else if (retirementAge <= currentAge) { errorMessage = "Retirement age must be greater than current age"; isValid = false; } break; case 2: // Family validation - currently optional break; case 3: const income = parseFloat(document.getElementById("annual-income").value); const expenses = parseFloat(document.getElementById("annual-expenses").value); if (!income || income <= 0) { errorMessage = "Please enter a valid annual income"; isValid = false; } else if (!expenses || expenses <= 0) { errorMessage = "Please enter valid annual expenses"; isValid = false; } else if (expenses > income * 2) { errorMessage = "Expenses seem unusually high compared to income. Please verify."; isValid = false; } break; case 4: // Goals validation - optional but check for realistic values const invalidGoals = goals.filter(goal => goal.cost <= 0 || goal.age < 18 || goal.age > 100 ); if (invalidGoals.length > 0) { errorMessage = "Some goals have invalid values. Please check cost and age."; isValid = false; } break; } if (!isValid) { showError(errorMessage); } return isValid; } function showError(message) { const errorDiv = document.getElementById("error-display"); const messageSpan = document.getElementById("error-message"); if (errorDiv && messageSpan) { messageSpan.textContent = message; errorDiv.style.display = "block"; errorDiv.setAttribute("role", "alert"); errorDiv.setAttribute("aria-live", "assertive"); // Scroll to error for visibility errorDiv.scrollIntoView({ behavior: "smooth", block: "nearest" }); // Focus on error for screen readers setTimeout(() => errorDiv.focus(), 100); } } function hideError() { const errorDiv = document.getElementById("error-display"); if (errorDiv) { errorDiv.style.display = "none"; errorDiv.removeAttribute("role"); errorDiv.removeAttribute("aria-live"); } } // Accessibility announcement function function announceStepChange(step) { const stepNames = [ "", "Basic Information", "Family Details", "Financial Information", "Goals Planning", "Results and Analysis" ]; const announcement = `Now on step ${step}: ${stepNames[step]}`; // Create or update announcement element let announcer = document.getElementById("step-announcer"); if (!announcer) { announcer = document.createElement("div"); announcer.id = "step-announcer"; announcer.setAttribute("aria-live", "polite"); announcer.setAttribute("aria-atomic", "true"); announcer.style.position = "absolute"; announcer.style.left = "-10000px"; announcer.style.width = "1px"; announcer.style.height = "1px"; announcer.style.overflow = "hidden"; document.body.appendChild(announcer); } announcer.textContent = announcement; } // Session persistence functions function saveProgress() { try { const progressData = { currentStep: currentStep, timestamp: Date.now(), formData: collectFormData() }; localStorage.setItem('qudos_progress', JSON.stringify(progressData)); } catch (error) { console.warn('Could not save progress:', error); } } function loadProgress() { try { const saved = localStorage.getItem('qudos_progress'); if (saved) { const progressData = JSON.parse(saved); // Check if data is less than 24 hours old const hoursSinceLastSave = (Date.now() - progressData.timestamp) / (1000 * 60 * 60); if (hoursSinceLastSave < 24) { return progressData; } } } catch (error) { console.warn('Could not load progress:', error); } return null; } // Timeline preview with enhanced formatting function updateTimelinePreview() { const currentAge = parseInt(document.getElementById("current-age").value) || 30; const retirementAge = parseInt(document.getElementById("retirement-age").value) || 65; const yearsToRetirement = retirementAge - currentAge; const preview = document.getElementById("timeline-preview"); if (preview) { if (yearsToRetirement > 0) { preview.textContent = `${yearsToRetirement} years until retirement`; preview.style.color = "#495057"; } else if (yearsToRetirement === 0) { preview.textContent = "Retirement age reached this year!"; preview.style.color = "#28a745"; } else { preview.textContent = "Already past retirement age"; preview.style.color = "#dc3545"; } } } // Partner details toggle with accessibility function togglePartnerDetails() { const hasPartner = document.getElementById("has-partner").value === "yes"; const ageContainer = document.getElementById("partner-age-container"); const incomeContainer = document.getElementById("partner-income-container"); if (ageContainer && incomeContainer) { ageContainer.style.display = hasPartner ? "block" : "none"; incomeContainer.style.display = hasPartner ? "block" : "none"; // Update ARIA attributes ageContainer.setAttribute("aria-hidden", !hasPartner); incomeContainer.setAttribute("aria-hidden", !hasPartner); // Focus management if (hasPartner) { const partnerAgeInput = document.getElementById("partner-age"); if (partnerAgeInput) { setTimeout(() => partnerAgeInput.focus(), 100); } } // Update cash flow when partner status changes updateCashFlow(); } } // Enhanced children inputs with validation function updateChildrenInputs() { const numChildren = parseInt(document.getElementById("num-children").value) || 0; const container = document.getElementById("children-container"); if (!container) return; container.innerHTML = ""; if (numChildren === 0) { container.innerHTML = `
No children to configure
`; return; } for (let i = 0; i < numChildren; i++) { const childDiv = document.createElement("div"); childDiv.className = "child-input-group"; childDiv.style = "display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px; padding: 15px; background: white; border-radius: 8px; border: 1px solid #e9ecef;"; childDiv.innerHTML = `
`; container.appendChild(childDiv); } // Add help text if (numChildren > 0) { const helpDiv = document.createElement("div"); helpDiv.innerHTML = ` Current age of child Annual expenses for this child `; container.appendChild(helpDiv); } } // Enhanced economic settings with real-time updates function updateEconomicSettings() { const settings = [ { id: 'tax-rate', display: 'tax-display', suffix: '%', color: '#dc3545' }, { id: 'inflation-rate', display: 'inflation-display', suffix: '%', color: '#e74c3c' }, { id: 'investment-return', display: 'investment-display', suffix: '%', color: '#6f42c1' }, { id: 'salary-growth', display: 'salary-display', suffix: '%', color: '#28a745' }, { id: 'emergency-target', display: 'emergency-display', suffix: '', color: '#17a2b8' } ]; settings.forEach(setting => { const element = document.getElementById(setting.id); const display = document.getElementById(setting.display); if (element && display) { const value = parseFloat(element.value); display.textContent = value + setting.suffix; display.style.color = setting.color; // Update ARIA label for accessibility element.setAttribute('aria-valuetext', `${value}${setting.suffix}`); // Add visual feedback for extreme values if (setting.id === 'tax-rate' && (value < 10 || value > 40)) { display.style.fontWeight = 'bold'; display.title = value < 10 ? 'Very low tax rate' : 'Very high tax rate'; } else if (setting.id === 'inflation-rate' && (value < 1 || value > 5)) { display.style.fontWeight = 'bold'; display.title = value < 1 ? 'Very low inflation' : 'High inflation scenario'; } else if (setting.id === 'investment-return' && (value < 4 || value > 10)) { display.style.fontWeight = 'bold'; display.title = value < 4 ? 'Conservative returns' : 'Aggressive returns'; } else { display.style.fontWeight = 'normal'; display.removeAttribute('title'); } } }); // Update cash flow when economic settings change updateCashFlow(); // Update scenario analysis if on results page if (currentStep === 5) { updateScenario(); } } // Comprehensive cash flow analysis with enhanced insights function updateCashFlow() { const income = parseFloat(document.getElementById("annual-income").value) || 0; const expenses = parseFloat(document.getElementById("annual-expenses").value) || 0; const monthlySavings = parseFloat(document.getElementById("monthly-savings").value) || 0; const taxRate = parseFloat(document.getElementById("tax-rate").value) || 25; const currentSavings = parseFloat(document.getElementById("current-savings").value) || 0; const totalDebt = parseFloat(document.getElementById("total-debt").value) || 0; const hasPartner = document.getElementById("has-partner").value === "yes"; const partnerIncome = hasPartner ? (parseFloat(document.getElementById("partner-income").value) || 0) : 0; // Calculate total household income const totalIncome = income + partnerIncome; const afterTaxIncome = totalIncome * (1 - taxRate / 100); const monthlyAfterTaxIncome = afterTaxIncome / 12; const monthlyExpenses = expenses / 12; const monthlyCashFlow = monthlyAfterTaxIncome - monthlyExpenses; const surplus = monthlyCashFlow - monthlySavings; // Calculate additional metrics const savingsRate = totalIncome > 0 ? (monthlySavings * 12 / totalIncome * 100) : 0; const expenseRatio = income > 0 ? (expenses / income * 100) : 0; const debtToIncomeRatio = totalIncome > 0 ? (totalDebt / totalIncome * 100) : 0; const emergencyFundMonths = monthlyExpenses > 0 ? (currentSavings / monthlyExpenses) : 0; // Update UI elements with enhanced formatting updateCashFlowDisplay(surplus, savingsRate, expenseRatio, debtToIncomeRatio, emergencyFundMonths); // Generate insights and recommendations generateFinancialInsights(surplus, savingsRate, expenseRatio, debtToIncomeRatio, emergencyFundMonths, totalIncome); } function updateCashFlowDisplay(surplus, savingsRate, expenseRatio, debtToIncomeRatio, emergencyFundMonths) { const preview = document.getElementById("cash-flow-preview"); const savingsRateDisplay = document.getElementById("savings-rate-display"); const nudgeMessage = document.getElementById("nudge-message"); if (preview) { const formattedSurplus = Math.round(Math.abs(surplus)).toLocaleString(); if (surplus >= 0) { preview.innerHTML = `+$${formattedSurplus} monthly surplus`; } else { preview.innerHTML = `-$${formattedSurplus} monthly shortfall`; } } if (savingsRateDisplay) { const rateColor = savingsRate >= 20 ? '#28a745' : savingsRate >= 10 ? '#ffc107' : '#dc3545'; savingsRateDisplay.innerHTML = ` Savings Rate: ${savingsRate.toFixed(1)}%
Emergency Fund: ${emergencyFundMonths.toFixed(1)} months • Expense Ratio: ${expenseRatio.toFixed(1)}% ${debtToIncomeRatio > 0 ? ` • Debt Ratio: ${debtToIncomeRatio.toFixed(1)}%` : ''} `; } } function generateFinancialInsights(surplus, savingsRate, expenseRatio, debtToIncomeRatio, emergencyFundMonths, totalIncome) { const nudgeMessage = document.getElementById("nudge-message"); if (!nudgeMessage) return; let message = ""; let icon = ""; let color = ""; // Priority-based recommendations if (surplus < -500) { message = "Critical: Monthly expenses exceed income. Immediate budget review needed."; icon = "🚨"; color = "#dc3545"; } else if (debtToIncomeRatio > 40) { message = "High debt burden detected. Consider debt consolidation or reduction strategies."; icon = "⚠️"; color = "#e74c3c"; } else if (emergencyFundMonths < 3) { message = "Build emergency fund first - aim for 3-6 months of expenses."; icon = "🛡️"; color = "#ff9800"; } else if (savingsRate < 10) { message = "Increase savings rate - financial experts recommend at least 10% of income."; icon = "💡"; color = "#ffc107"; } else if (savingsRate < 15) { message = "Good progress! Consider increasing savings to 15-20% for faster wealth building."; icon = "📈"; color = "#17a2b8"; } else if (savingsRate >= 20) { message = "Excellent savings discipline! You're on track for financial independence."; icon = "✅"; color = "#28a745"; } else { message = "Solid financial foundation. Keep up the consistent saving habits!"; icon = "👍"; color = "#28a745"; } nudgeMessage.innerHTML = ` ${icon} ${message} `; // Add detailed breakdown for advanced users const detailsDiv = document.getElementById("cash-flow-details"); if (detailsDiv) { detailsDiv.innerHTML = `
Savings Rate
${savingsRate.toFixed(1)}%
Emergency Fund
${emergencyFundMonths.toFixed(1)} months
Expense Ratio
${expenseRatio.toFixed(1)}%
${debtToIncomeRatio > 0 ? `
Debt Ratio
${debtToIncomeRatio.toFixed(1)}%
` : ''}
`; } } // Enhanced language switching with persistence function switchLanguage(lang) { currentLanguage = lang; // Save language preference try { localStorage.setItem('qudos_language', lang); } catch (error) { console.warn('Could not save language preference:', error); } // Update UI text (simplified - in production would use i18next) updateUILanguage(lang); // Update charts if they exist if (chartInstance) { updateChartLanguage(lang); } } function updateUILanguage(lang) { // This would integrate with i18next in production const langTexts = { 'en': { title: 'Qudos Coin Ultimate', subtitle: 'Professional Financial Planning' }, 'es': { title: 'Qudos Coin Ultimate', subtitle: 'Planificación Financiera Profesional' }, 'zh': { title: 'Qudos 终极理财', subtitle: '专业财务规划' } }; const texts = langTexts[lang] || langTexts['en']; // Update main title const titleElement = document.querySelector('h1'); if (titleElement) { titleElement.textContent = texts.title; } // Update subtitle const subtitleElement = document.querySelector('p'); if (subtitleElement && subtitleElement.textContent.includes('Professional')) { subtitleElement.textContent = texts.subtitle; } } // Accessibility enhancements function initializeAccessibility() { // Add keyboard navigation for sliders document.querySelectorAll('input[type="range"]').forEach(slider => { slider.addEventListener('keydown', function(e) { const step = parseFloat(this.step) || 1; const min = parseFloat(this.min) || 0; const max = parseFloat(this.max) || 100; let value = parseFloat(this.value); switch(e.key) { case 'ArrowUp': case 'ArrowRight': value = Math.min(max, value + step); break; case 'ArrowDown': case 'ArrowLeft': value = Math.max(min, value - step); break; case 'Home': value = min; break; case 'End': value = max; break; default: return; } e.preventDefault(); this.value = value; this.dispatchEvent(new Event('input', { bubbles: true })); this.dispatchEvent(new Event('change', { bubbles: true })); }); }); // Add high contrast mode toggle const contrastToggle = document.getElementById('high-contrast-toggle'); if (contrastToggle) { contrastToggle.addEventListener('click', toggleHighContrast); } } function toggleHighContrast() { const body = document.body; const isHighContrast = body.classList.contains('high-contrast'); if (isHighContrast) { body.classList.remove('high-contrast'); localStorage.setItem('qudos_high_contrast', 'false'); } else { body.classList.add('high-contrast'); localStorage.setItem('qudos_high_contrast', 'true'); } } // Initialize accessibility on page load function initializeAccessibilityFeatures() { // Load high contrast preference const highContrastPref = localStorage.getItem('qudos_high_contrast'); if (highContrastPref === 'true') { document.body.classList.add('high-contrast'); } // Load language preference const langPref = localStorage.getItem('qudos_language'); if (langPref && langPref !== currentLanguage) { switchLanguage(langPref); } // Initialize keyboard navigation initializeAccessibility(); } // Enhanced goals management with validation and insights function addQuickGoal(type) { const template = goalTemplates[type]; if (!template) { console.error('Invalid goal type:', type); return; } const currentAge = parseInt(document.getElementById("current-age").value) || 30; const retirementAge = parseInt(document.getElementById("retirement-age").value) || 65; const targetAge = Math.min(currentAge + template.ageOffset, retirementAge - 1); const goal = { id: Date.now() + Math.random(), // Ensure uniqueness name: template.name, cost: template.cost, age: targetAge, icon: template.icon, priority: goals.length + 1, category: type, dateAdded: new Date().toISOString() }; goals.push(goal); updateGoalsList(); saveProgress(); // Analytics tracking trackGoalAdded(type, goal.cost); // Announce to screen readers announceGoalChange('added', goal.name); } function addCustomGoal() { const modal = document.getElementById("custom-goal-modal"); if (modal) { modal.style.display = "flex"; modal.setAttribute("aria-hidden", "false"); // Focus on first input const firstInput = modal.querySelector('input'); if (firstInput) { setTimeout(() => firstInput.focus(), 100); } // Trap focus within modal trapFocus(modal); } } function closeCustomGoalModal() { const modal = document.getElementById("custom-goal-modal"); if (modal) { modal.style.display = "none"; modal.setAttribute("aria-hidden", "true"); // Return focus to add button const addButton = document.querySelector('[onclick="addCustomGoal()"]'); if (addButton) { addButton.focus(); } // Clear form resetCustomGoalForm(); } } function saveCustomGoal() { const name = document.getElementById("custom-goal-name").value.trim(); const cost = parseFloat(document.getElementById("custom-goal-cost").value); const age = parseInt(document.getElementById("custom-goal-age").value); // Enhanced validation const validationErrors = validateCustomGoal(name, cost, age); if (validationErrors.length > 0) { showCustomGoalErrors(validationErrors); return; } const goal = { id: Date.now() + Math.random(), name: name, cost: cost, age: age, icon: "🎯", priority: goals.length + 1, category: 'custom', dateAdded: new Date().toISOString() }; goals.push(goal); updateGoalsList(); closeCustomGoalModal(); saveProgress(); // Analytics and announcement trackGoalAdded('custom', goal.cost); announceGoalChange('added', goal.name); } function validateCustomGoal(name, cost, age) { const errors = []; const currentAge = parseInt(document.getElementById("current-age").value) || 30; const retirementAge = parseInt(document.getElementById("retirement-age").value) || 65; if (!name || name.length < 2) { errors.push("Goal name must be at least 2 characters long"); } if (name.length > 50) { errors.push("Goal name must be less than 50 characters"); } if (!cost || cost < 100) { errors.push("Goal cost must be at least $100"); } if (cost > 10000000) { errors.push("Goal cost seems unrealistic. Please verify."); } if (!age || age < currentAge) { errors.push("Target age must be in the future"); } if (age > retirementAge) { errors.push("Target age should be before retirement"); } // Check for duplicate names if (goals.some(g => g.name.toLowerCase() === name.toLowerCase())) { errors.push("A goal with this name already exists"); } return errors; } function showCustomGoalErrors(errors) { const errorContainer = document.getElementById("custom-goal-errors"); if (errorContainer) { errorContainer.innerHTML = errors.map(error => `
• ${error}
` ).join(''); errorContainer.style.display = "block"; errorContainer.setAttribute("role", "alert"); } } function resetCustomGoalForm() { document.getElementById("custom-goal-name").value = ""; document.getElementById("custom-goal-cost").value = "10000"; document.getElementById("custom-goal-age").value = "35"; const errorContainer = document.getElementById("custom-goal-errors"); if (errorContainer) { errorContainer.style.display = "none"; errorContainer.innerHTML = ""; } } function removeGoal(goalId) { const goalIndex = goals.findIndex(goal => goal.id === goalId); if (goalIndex === -1) return; const removedGoal = goals[goalIndex]; goals.splice(goalIndex, 1); // Update priorities goals.forEach((goal, index) => { goal.priority = index + 1; }); updateGoalsList(); saveProgress(); // Analytics and announcement trackGoalRemoved(removedGoal.category, removedGoal.cost); announceGoalChange('removed', removedGoal.name); } function updateGoalsList() { const container = document.getElementById("goals-list"); if (!container) return; if (goals.length === 0) { container.innerHTML = `
🎯

No goals added yet

Click the buttons above to add your first goal!

`; return; } // Sort goals by priority const sortedGoals = [...goals].sort((a, b) => a.priority - b.priority); const goalsHtml = sortedGoals.map((goal, index) => createGoalHTML(goal, index)).join(""); container.innerHTML = `
${goalsHtml}
`; // Initialize drag and drop initializeDragAndDrop(); } function createGoalHTML(goal, index) { const currentAge = parseInt(document.getElementById("current-age").value) || 30; const yearsToGoal = goal.age - currentAge; const urgencyColor = yearsToGoal <= 2 ? '#dc3545' : yearsToGoal <= 5 ? '#ffc107' : '#28a745'; return `
⋮⋮
${index + 1}
${goal.icon}
${goal.name}
Priority ${index + 1} • Age ${goal.age} • $${goal.cost.toLocaleString()} ${yearsToGoal > 0 ? `• ${yearsToGoal} years` : '• Overdue'}
`; } // Enhanced drag and drop with accessibility let draggedElement = null; let draggedIndex = null; function initializeDragAndDrop() { document.querySelectorAll('.goal-item').forEach(item => { item.addEventListener('dragstart', handleDragStart); item.addEventListener('dragover', handleDragOver); item.addEventListener('drop', handleDrop); item.addEventListener('dragend', handleDragEnd); // Keyboard support for reordering item.addEventListener('keydown', handleKeyboardReorder); }); } function handleDragStart(e) { draggedElement = e.target.closest('.goal-item'); draggedIndex = parseInt(draggedElement.dataset.index); e.target.style.opacity = '0.5'; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/html', draggedElement.outerHTML); } function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const targetElement = e.target.closest('.goal-item'); if (targetElement && targetElement !== draggedElement) { targetElement.style.borderTop = '3px solid #667eea'; } } function handleDrop(e) { e.preventDefault(); const targetElement = e.target.closest('.goal-item'); if (targetElement && targetElement !== draggedElement) { const targetIndex = parseInt(targetElement.dataset.index); // Reorder goals array const draggedGoal = goals.splice(draggedIndex, 1)[0]; goals.splice(targetIndex, 0, draggedGoal); // Update priorities goals.forEach((goal, index) => { goal.priority = index + 1; }); updateGoalsList(); saveProgress(); // Announce reorder announceGoalChange('reordered', draggedGoal.name); } handleDragEnd(); } function handleDragEnd() { document.querySelectorAll('.goal-item').forEach(el => { el.style.opacity = '1'; el.style.borderTop = ''; }); draggedElement = null; draggedIndex = null; } function handleKeyboardReorder(e) { if (e.key === 'ArrowUp' && e.ctrlKey) { e.preventDefault(); moveGoalUp(parseInt(e.target.dataset.index)); } else if (e.key === 'ArrowDown' && e.ctrlKey) { e.preventDefault(); moveGoalDown(parseInt(e.target.dataset.index)); } } function moveGoalUp(index) { if (index > 0) { [goals[index], goals[index - 1]] = [goals[index - 1], goals[index]]; goals.forEach((goal, i) => goal.priority = i + 1); updateGoalsList(); saveProgress(); } } function moveGoalDown(index) { if (index < goals.length - 1) { [goals[index], goals[index + 1]] = [goals[index + 1], goals[index]]; goals.forEach((goal, i) => goal.priority = i + 1); updateGoalsList(); saveProgress(); } } // Enhanced scenario analysis with predictive modeling function updateScenario() { const incomeChange = parseFloat(document.getElementById("income-change").value) || 0; const marketChange = parseFloat(document.getElementById("market-change").value) || 0; const inflationChange = parseFloat(document.getElementById("inflation-change")?.value) || 0; // Update display values document.getElementById("income-change-display").textContent = (incomeChange >= 0 ? '+' : '') + incomeChange + '%'; const marketLabels = { '-5': 'Severe Bear Market (-5%)', '-2.5': 'Bear Market (-2.5%)', '0': 'Normal Market (0%)', '2.5': 'Bull Market (+2.5%)', '5': 'Extreme Bull Market (+5%)' }; document.getElementById("market-change-display").textContent = marketLabels[marketChange] || `Market: ${marketChange}%`; // Calculate comprehensive impact const baseData = collectFormData(); const scenarioData = createScenarioData(baseData, incomeChange, marketChange, inflationChange); const baseProjection = calculateQuickProjection(baseData); const scenarioProjection = calculateQuickProjection(scenarioData); const impact = calculateScenarioImpact(baseProjection, scenarioProjection); updateScenarioDisplay(impact); // Update chart if visible if (chartInstance && currentStep === 5) { updateChartWithScenario(scenarioProjection); } } function createScenarioData(baseData, incomeChange, marketChange, inflationChange) { return { ...baseData, annualIncome: baseData.annualIncome * (1 + incomeChange / 100), partnerIncome: baseData.partnerIncome * (1 + incomeChange / 100), investmentReturn: baseData.investmentReturn + (marketChange / 100), inflationRate: baseData.inflationRate + (inflationChange / 100) }; } function calculateScenarioImpact(base, scenario) { const retirementDifference = scenario.finalSavings - base.finalSavings; const percentageChange = base.finalSavings > 0 ? (retirementDifference / base.finalSavings * 100) : 0; return { retirementDifference, percentageChange, finalSavings: scenario.finalSavings, yearsToTarget: scenario.yearsToTarget, riskLevel: assessRiskLevel(scenario) }; } function updateScenarioDisplay(impact) { const impactDisplay = document.getElementById("impact-display"); const scenarioSummary = document.getElementById("scenario-summary"); if (impactDisplay) { const sign = impact.retirementDifference >= 0 ? '+' : ''; const color = impact.retirementDifference >= 0 ? '#28a745' : '#dc3545'; impactDisplay.innerHTML = `
${sign}${formatCurrency(impact.retirementDifference)}
${sign}${impact.percentageChange.toFixed(1)}% change
`; } if (scenarioSummary) { scenarioSummary.innerHTML = `
Projected Retirement
${formatCurrency(impact.finalSavings)}
Risk Level
${impact.riskLevel}
`; } } function assessRiskLevel(data) { const savingsRate = (data.monthlySavings * 12) / data.annualIncome; const debtRatio = data.totalDebt / data.annualIncome; const marketRisk = Math.abs(data.investmentReturn - 0.07); // Deviation from 7% baseline let riskScore = 0; if (savingsRate < 0.1) riskScore += 2; else if (savingsRate < 0.15) riskScore += 1; if (debtRatio > 0.4) riskScore += 2; else if (debtRatio > 0.2) riskScore += 1; if (marketRisk > 0.03) riskScore += 1; if (riskScore >= 3) return 'High'; if (riskScore >= 2) return 'Medium'; return 'Low'; } function getRiskColor(riskLevel) { switch(riskLevel) { case 'High': return '#dc3545'; case 'Medium': return '#ffc107'; case 'Low': return '#28a745'; default: return '#6c757d'; } } // Utility functions for goals and scenarios function announceGoalChange(action, goalName) { const messages = { 'added': `Goal "${goalName}" has been added to your plan`, 'removed': `Goal "${goalName}" has been removed from your plan`, 'reordered': `Goal "${goalName}" has been moved to a new position` }; const announcement = messages[action] || `Goal "${goalName}" has been updated`; let announcer = document.getElementById("goal-announcer"); if (!announcer) { announcer = document.createElement("div"); announcer.id = "goal-announcer"; announcer.setAttribute("aria-live", "polite"); announcer.setAttribute("aria-atomic", "true"); announcer.style.position = "absolute"; announcer.style.left = "-10000px"; announcer.style.width = "1px"; announcer.style.height = "1px"; announcer.style.overflow = "hidden"; document.body.appendChild(announcer); } announcer.textContent = announcement; } function trapFocus(element) { const focusableElements = element.querySelectorAll( 'button, input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; element.addEventListener('keydown', function(e) { if (e.key === 'Tab') { if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } else if (e.key === 'Escape') { closeCustomGoalModal(); } }); } // Analytics tracking functions (placeholder for actual implementation) function trackGoalAdded(type, cost) { // Integration point for analytics console.log('Goal added:', { type, cost, timestamp: new Date().toISOString() }); } function trackGoalRemoved(type, cost) { // Integration point for analytics console.log('Goal removed:', { type, cost, timestamp: new Date().toISOString() }); } // Enhanced results generation with Monte Carlo simulation function generateResults() { console.log('Generating comprehensive financial results...'); // Validate all data before generation if (!validateAllSteps()) { showError('Please complete all required fields before generating results.'); return; } // Show loading with progress updates showLoadingProgress(); // Start comprehensive calculation process setTimeout(() => { try { const data = collectFormData(); const results = calculateComprehensiveProjection(data); displayResults(results); initializeMonteCarloSimulation(data); // Hide loading and show results hideLoadingProgress(); currentStep = 5; showStep(5); updateProgress(); // Generate PDF export data prepareExportData(results); } catch (error) { console.error('Error generating results:', error); showError('An error occurred while generating your financial plan. Please try again.'); hideLoadingProgress(); } }, 1000); // Reduced delay for better UX } function validateAllSteps() { for (let step = 1; step <= 4; step++) { const oldStep = currentStep; currentStep = step; if (!validateCurrentStep()) { currentStep = oldStep; showStep(step); // Show the problematic step return false; } } return true; } function showLoadingProgress() { const loadingDiv = document.getElementById('loading-display'); const currentStepDiv = document.querySelector('.wizard-step[data-step="4"]'); if (loadingDiv && currentStepDiv) { currentStepDiv.style.display = 'none'; loadingDiv.style.display = 'block'; // Enhanced loading animation with progress steps const loadingSteps = [ 'Analyzing financial data...', 'Running Monte Carlo simulations...', 'Calculating goal timelines...', 'Generating insights...', 'Preparing visualization...' ]; let stepIndex = 0; const loadingText = loadingDiv.querySelector('p'); const progressInterval = setInterval(() => { if (stepIndex < loadingSteps.length) { loadingText.textContent = loadingSteps[stepIndex]; stepIndex++; } else { clearInterval(progressInterval); } }, 400); // Store interval for cleanup loadingDiv.dataset.progressInterval = progressInterval; } } function hideLoadingProgress() { const loadingDiv = document.getElementById('loading-display'); if (loadingDiv) { loadingDiv.style.display = 'none'; // Clear progress interval const interval = loadingDiv.dataset.progressInterval; if (interval) { clearInterval(parseInt(interval)); } } } function collectFormData() { const data = { // Basic information currentAge: parseInt(document.getElementById('current-age').value) || 30, retirementAge: parseInt(document.getElementById('retirement-age').value) || 65, // Financial information annualIncome: parseFloat(document.getElementById('annual-income').value) || 0, annualExpenses: parseFloat(document.getElementById('annual-expenses').value) || 0, monthlySavings: parseFloat(document.getElementById('monthly-savings').value) || 0, currentSavings: parseFloat(document.getElementById('current-savings').value) || 0, totalDebt: parseFloat(document.getElementById('total-debt').value) || 0, debtRate: parseFloat(document.getElementById('debt-rate').value) / 100 || 0, // Economic assumptions taxRate: parseFloat(document.getElementById('tax-rate').value) / 100 || 0.25, inflationRate: parseFloat(document.getElementById('inflation-rate').value) / 100 || 0.029, investmentReturn: parseFloat(document.getElementById('investment-return').value) / 100 || 0.065, salaryGrowth: parseFloat(document.getElementById('salary-growth').value) / 100 || 0.039, emergencyTarget: parseInt(document.getElementById('emergency-target').value) || 6, // Family information hasPartner: document.getElementById('has-partner').value === 'yes', partnerAge: parseInt(document.getElementById('partner-age')?.value) || 0, partnerIncome: parseFloat(document.getElementById('partner-income')?.value) || 0, numChildren: parseInt(document.getElementById('num-children').value) || 0, // Goals goals: [...goals], // Create a copy // Metadata calculationDate: new Date().toISOString(), version: qudosAjax.version || '5.2.0' }; // Collect children data data.children = []; for (let i = 0; i < data.numChildren; i++) { const ageInput = document.getElementById(`child-${i}-age`); const costInput = document.getElementById(`child-${i}-cost`); if (ageInput && costInput) { data.children.push({ age: parseInt(ageInput.value) || 5, annualCost: parseFloat(costInput.value) || 15000 }); } } return data; } function calculateComprehensiveProjection(data) { const years = []; const netWorthData = []; const incomeData = []; const expenseData = []; const savingsData = []; const goalMarkers = []; let totalSavings = data.currentSavings; let remainingDebt = data.totalDebt; const yearsToRetirement = data.retirementAge - data.currentAge; // Enhanced projection with more sophisticated modeling for (let age = data.currentAge; age <= data.currentAge + 50; age++) { const yearIndex = age - data.currentAge; years.push(age); let annualIncome = 0; let annualExpenses = data.annualExpenses * Math.pow(1 + data.inflationRate, yearIndex); let annualSavings = 0; // Add children costs data.children.forEach(child => { const childAge = child.age + yearIndex; if (childAge >= 0 && childAge <= 25) { annualExpenses += child.annualCost * Math.pow(1 + data.inflationRate, yearIndex); } }); // Add goal costs for this year goals.forEach(goal => { if (goal.age === age) { const inflatedCost = goal.cost * Math.pow(1 + data.inflationRate, yearIndex); annualExpenses += inflatedCost; goalMarkers.push({ age: age, cost: inflatedCost, name: goal.name, icon: goal.icon, originalCost: goal.cost }); } }); if (age < data.retirementAge) { // Working years let grossIncome = data.annualIncome * Math.pow(1 + data.salaryGrowth, yearIndex); // Add partner income if (data.hasPartner) { grossIncome += data.partnerIncome * Math.pow(1 + data.salaryGrowth, yearIndex); } annualIncome = grossIncome * (1 - data.taxRate); // Debt servicing if (remainingDebt > 0) { const debtPayment = Math.min(remainingDebt * (0.2 + data.debtRate), remainingDebt); annualExpenses += debtPayment; remainingDebt = Math.max(0, remainingDebt - debtPayment); } // Calculate savings const plannedSavings = data.monthlySavings * 12; const availableForSavings = annualIncome - annualExpenses; annualSavings = Math.min(plannedSavings, Math.max(0, availableForSavings)); // Investment growth totalSavings = totalSavings * (1 + data.investmentReturn) + annualSavings; } else { // Retirement years - enhanced withdrawal strategy const targetWithdrawal = annualExpenses; const safeWithdrawalRate = 0.04; // 4% rule const maxSafeWithdrawal = totalSavings * safeWithdrawalRate; const actualWithdrawal = Math.min(targetWithdrawal, maxSafeWithdrawal); totalSavings = Math.max(0, totalSavings * (1 + data.investmentReturn) - actualWithdrawal); annualIncome = actualWithdrawal; annualSavings = 0; } // Cap at reasonable values totalSavings = Math.min(totalSavings, 100000000); // Store data netWorthData.push(Math.max(0, totalSavings - remainingDebt)); incomeData.push(annualIncome); expenseData.push(annualExpenses); savingsData.push(annualSavings); } // Calculate additional metrics const emergencyFundTarget = (data.annualExpenses / 12) * data.emergencyTarget; const emergencyFundRatio = data.currentSavings / emergencyFundTarget; return { years, netWorthData, incomeData, expenseData, savingsData, goalMarkers, finalNetWorth: netWorthData[netWorthData.length - 1], finalSavings: totalSavings, remainingDebt, emergencyFundRatio, emergencyFundTarget, yearsToRetirement, projectionSummary: generateProjectionSummary(netWorthData, totalSavings, data) }; } function generateProjectionSummary(netWorthData, finalSavings, data) { const peakNetWorth = Math.max(...netWorthData); const retirementNetWorth = netWorthData[data.retirementAge - data.currentAge] || 0; const currentSavingsRate = (data.monthlySavings * 12) / data.annualIncome; return { peakNetWorth, retirementNetWorth, finalSavings, currentSavingsRate, projectedSavingsRate: currentSavingsRate, financialIndependenceAge: calculateFIAge(netWorthData, data), riskScore: calculateRiskScore(data) }; } function calculateFIAge(netWorthData, data) { const fiTarget = data.annualExpenses * 25; // 25x annual expenses for (let i = 0; i < netWorthData.length; i++) { if (netWorthData[i] >= fiTarget) { return data.currentAge + i; } } return null; // FI not achieved in projection period } function calculateRiskScore(data) { let score = 0; // Savings rate risk const savingsRate = (data.monthlySavings * 12) / data.annualIncome; if (savingsRate < 0.1) score += 3; else if (savingsRate < 0.15) score += 1; // Debt risk const debtRatio = data.totalDebt / data.annualIncome; if (debtRatio > 0.4) score += 3; else if (debtRatio > 0.2) score += 1; // Emergency fund risk const emergencyMonths = data.currentSavings / (data.annualExpenses / 12); if (emergencyMonths < 3) score += 2; else if (emergencyMonths < 6) score += 1; // Investment risk if (data.investmentReturn > 0.08) score += 1; if (data.investmentReturn < 0.04) score += 1; return Math.min(10, score); } function displayResults(results) { calculationResults = results; // Update summary cards with enhanced data updateSummaryCards(results); // Create comprehensive chart setTimeout(() => { const chartContainer = document.getElementById('chart-container'); if (chartContainer && chartContainer.offsetHeight > 0) { createEnhancedChart(results); } else { console.error('Chart container not ready'); showChartError(); } }, 100); // Update scenario analysis initializeScenarioAnalysis(results); } function updateSummaryCards(results) { const summary = results.projectionSummary; // Retirement Savings const retirementTotal = document.getElementById('retirement-total'); if (retirementTotal) { retirementTotal.textContent = formatCurrency(results.finalSavings); } const retirementReal = document.getElementById('retirement-real'); if (retirementReal) { const realValue = results.finalSavings / Math.pow(1.03, results.yearsToRetirement); retirementReal.textContent = `(${formatCurrency(realValue)} in today's purchasing power)`; } // Financial Health Score const healthScore = document.getElementById('health-score'); const healthDetails = document.getElementById('health-details'); if (healthScore && healthDetails) { const score = calculateHealthScore(summary); healthScore.textContent = score.level; healthScore.style.color = score.color; healthDetails.textContent = score.description; } // Goals Status const goalsStatus = document.getElementById('goals-status'); const goalsDetails = document.getElementById('goals-details'); if (goalsStatus && goalsDetails) { const goalsCount = goals.length; const affordableGoals = calculateAffordableGoals(results); goalsStatus.textContent = goalsCount > 0 ? `${affordableGoals}/${goalsCount} Goals Achievable` : 'No Goals Set'; goalsDetails.textContent = goalsCount > 0 ? `Based on current savings trajectory` : 'Consider adding financial goals'; } // Success Rate (simplified Monte Carlo preview) const successRate = document.getElementById('success-rate'); if (successRate) { const rate = Math.max(25, Math.min(95, 85 - summary.riskScore * 5)); successRate.textContent = rate + '%'; } } function calculateHealthScore(summary) { let score = 100; // Deduct points for various risk factors score -= summary.riskScore * 10; // Savings rate impact if (summary.currentSavingsRate < 0.1) score -= 20; else if (summary.currentSavingsRate < 0.15) score -= 10; // FI age impact if (!summary.financialIndependenceAge) score -= 15; else if (summary.financialIndependenceAge > 65) score -= 5; score = Math.max(0, Math.min(100, score)); if (score >= 80) return { level: 'Excellent', color: '#28a745', description: 'Strong financial position' }; if (score >= 60) return { level: 'Good', color: '#17a2b8', description: 'Solid financial foundation' }; if (score >= 40) return { level: 'Fair', color: '#ffc107', description: 'Room for improvement' }; return { level: 'Needs Work', color: '#dc3545', description: 'Requires attention' }; } function calculateAffordableGoals(results) { let affordable = 0; const maxAge = Math.min(results.years[results.years.length - 1], results.years[0] + 40); goals.forEach(goal => { if (goal.age <= maxAge) { const yearIndex = goal.age - results.years[0]; if (yearIndex >= 0 && yearIndex < results.netWorthData.length) { const netWorthAtGoal = results.netWorthData[yearIndex]; if (netWorthAtGoal >= goal.cost) { affordable++; } } } }); return affordable; } function createEnhancedChart(data) { const canvas = document.getElementById('results-chart'); if (!canvas) { console.error('Chart canvas not found'); return; } const ctx = canvas.getContext('2d'); if (!ctx) { console.error('Failed to get canvas context'); return; } // Properly destroy existing chart if (chartInstance) { chartInstance.destroy(); chartInstance = null; } // Wait for Chart.js to be available waitForChartJs(() => { try { chartInstance = new Chart(ctx, { type: 'line', data: { labels: data.years, datasets: [ { label: 'Net Worth', data: data.netWorthData || [], borderColor: '#28a745', backgroundColor: 'rgba(40, 167, 69, 0.1)', fill: true, tension: 0.4, borderWidth: 3, pointRadius: 0, pointHoverRadius: 5 }, { label: 'Annual Income', data: data.incomeData || [], borderColor: '#007bff', backgroundColor: 'rgba(0, 123, 255, 0.05)', fill: false, tension: 0.4, borderWidth: 2, pointRadius: 0, pointHoverRadius: 3 }, { label: 'Annual Expenses', data: data.expenseData || [], borderColor: '#dc3545', backgroundColor: 'rgba(220, 53, 69, 0.05)', fill: false, tension: 0.4, borderWidth: 2, pointRadius: 0, pointHoverRadius: 3 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { intersect: false, mode: 'index' }, plugins: { title: { display: true, text: 'Your Financial Journey', font: { size: 16, weight: 'bold' } }, tooltip: { callbacks: { title: function(context) { return `Age ${context[0].label}`; }, label: function(context) { return `${context.dataset.label}: ${formatCurrency(context.parsed.y)}`; }, afterBody: function(context) { const age = parseInt(context[0].label); const goalAtAge = data.goalMarkers.find(g => g.age === age); if (goalAtAge) { return [`🎯 Goal: ${goalAtAge.name} (${formatCurrency(goalAtAge.cost)})`]; } return []; } } }, legend: { position: 'bottom', labels: { usePointStyle: true, padding: 20 } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return formatCurrency(value); } }, grid: { color: 'rgba(0,0,0,0.1)' } }, x: { title: { display: true, text: 'Age', font: { weight: 'bold' } }, grid: { color: 'rgba(0,0,0,0.1)' } } } } }); // Add goal markers addGoalMarkersToChart(data.goalMarkers); } catch (error) { console.error('Failed to create chart:', error); showChartError(); } }); } function addGoalMarkersToChart(goalMarkers) { if (!chartInstance || !goalMarkers.length) return; // Add goal markers as annotations (simplified version) goalMarkers.forEach(goal => { const yearIndex = chartInstance.data.labels.indexOf(goal.age); if (yearIndex !== -1) { // This would be enhanced with Chart.js annotation plugin in production console.log(`Goal marker: ${goal.name} at age ${goal.age}`); } }); } // Utility functions function formatCurrency(amount) { if (isNaN(amount) || amount === null || amount === undefined) return '$0'; const absAmount = Math.abs(amount); const sign = amount < 0 ? '-' : ''; if (absAmount >= 1000000) { return sign + '$' + (absAmount / 1000000).toFixed(1) + 'M'; } if (absAmount >= 1000) { return sign + '$' + (absAmount / 1000).toFixed(0) + 'K'; } return sign + '$' + Math.round(absAmount).toLocaleString(); } function calculateQuickProjection(data) { // Simplified projection for scenario analysis const years = data.retirementAge - data.currentAge; let savings = data.currentSavings; for (let i = 0; i < years; i++) { savings = savings * (1 + data.investmentReturn) + (data.monthlySavings * 12); } return { finalSavings: Math.max(0, savings), yearsToTarget: years }; } function prepareExportData(results) { // Prepare data for PDF export window.qudosExportData = { results: results, timestamp: new Date().toISOString(), user: { currentAge: results.years[0], retirementAge: results.years[0] + results.yearsToRetirement } }; } // Enhanced export function with Australian date formatting function exportPlan() { const currentDate = new Date(); const dateString = currentDate.toLocaleDateString('en-AU', { timeZone: 'Australia/Perth', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true }); // In production, this would generate an actual PDF alert(`Export functionality would generate a comprehensive PDF report of your financial plan.\n\n` + `Generated on: ${dateString} AWST\n` + `Report includes: Financial projections, goal analysis, risk assessment, and recommendations.`); } function toggleFullscreen() { const chartContainer = document.getElementById('chart-container'); if (!chartContainer) return; if (document.fullscreenElement) { document.exitFullscreen(); } else { chartContainer.requestFullscreen().catch(err => { console.log('Fullscreen not supported:', err); }); } } // Initialize Monte Carlo simulation in Web Worker function initializeMonteCarloSimulation(data) { if (!calculationWorker) { setupWebWorker(); } if (calculationWorker) { calculationWorker.postMessage({ type: 'monte-carlo', data: { currentSavings: data.currentSavings, monthlySavings: data.monthlySavings, investmentReturn: data.investmentReturn, retirementAge: data.retirementAge, currentAge: data.currentAge, annualExpenses: data.annualExpenses } }); } } function setupWebWorker() { if (typeof Worker === 'undefined') { console.warn('Web Workers not supported'); return; } try { const workerCode = ` self.onmessage = function(e) { const { type, data } = e.data; if (type === 'monte-carlo') { const results = runMonteCarloSimulation(data); self.postMessage({ type: 'monte-carlo-result', results }); } }; function runMonteCarloSimulation(data) { const trials = 1000; const outcomes = []; const years = data.retirementAge - data.currentAge; for (let trial = 0; trial < trials; trial++) { let savings = data.currentSavings; for (let year = 0; year < years; year++) { // Random return with normal distribution (simplified) const randomReturn = (Math.random() - 0.5) * 0.3 + data.investmentReturn; savings = savings * (1 + randomReturn) + (data.monthlySavings * 12); } outcomes.push(Math.max(0, savings)); } outcomes.sort((a, b) => a - b); const target = data.annualExpenses * 25; // 25x expenses rule const successfulOutcomes = outcomes.filter(outcome => outcome >= target); return { percentile10: outcomes[Math.floor(trials * 0.1)], percentile50: outcomes[Math.floor(trials * 0.5)], percentile90: outcomes[Math.floor(trials * 0.9)], mean: outcomes.reduce((a, b) => a + b, 0) / outcomes.length, successRate: (successfulOutcomes.length / trials) * 100 }; } `; const blob = new Blob([workerCode], { type: 'application/javascript' }); calculationWorker = new Worker(URL.createObjectURL(blob)); calculationWorker.onmessage = function(e) { const { type, results } = e.data; if (type === 'monte-carlo-result') { updateMonteCarloResults(results); } }; calculationWorker.onerror = function(error) { console.error('Worker error:', error); }; } catch (error) { console.error('Failed to create Web Worker:', error); } } function updateMonteCarloResults(results) { const successRate = document.getElementById('success-rate'); if (successRate) { successRate.textContent = Math.round(results.successRate) + '%'; } // Update additional Monte Carlo displays if they exist const monteCarloDetails = document.getElementById('monte-carlo-details'); if (monteCarloDetails) { monteCarloDetails.innerHTML = `
10th Percentile
${formatCurrency(results.percentile10)}
Expected (50th)
${formatCurrency(results.percentile50)}
90th Percentile
${formatCurrency(results.percentile90)}
`; } } // Dark mode and accessibility functions function toggleDarkMode() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); // Update button icon and text const button = document.querySelector('[onclick="toggleDarkMode()"]'); if (button) { button.innerHTML = newTheme === 'dark' ? '☀️' : '🌙'; button.setAttribute('aria-label', newTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'); } // Save preference try { localStorage.setItem('qudos_theme', newTheme); } catch (error) { console.warn('Could not save theme preference:', error); } // Update chart colors if chart exists if (chartInstance) { updateChartTheme(newTheme); } } function updateChartTheme(theme) { if (!chartInstance) return; const isDark = theme === 'dark'; const textColor = isDark ? '#ffffff' : '#666666'; const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; chartInstance.options.plugins.title.color = textColor; chartInstance.options.plugins.legend.labels.color = textColor; chartInstance.options.scales.x.ticks.color = textColor; chartInstance.options.scales.y.ticks.color = textColor; chartInstance.options.scales.x.grid.color = gridColor; chartInstance.options.scales.y.grid.color = gridColor; chartInstance.update(); } // Community features async function shareStrategy() { const anonymizedPlan = { goals: goals.map(goal => ({ category: goal.category, targetAge: goal.age, // Remove specific amounts and names for privacy relativeSize: goal.cost > 50000 ? 'large' : goal.cost > 20000 ? 'medium' : 'small' })), demographics: { ageRange: getAgeRange(parseInt(document.getElementById('current-age').value)), hasPartner: document.getElementById('has-partner').value === 'yes', hasChildren: parseInt(document.getElementById('num-children').value) > 0 }, strategy: { savingsRateCategory: getSavingsRateCategory(), riskLevel: document.getElementById('investment-return').value > 7 ? 'aggressive' : 'conservative' } }; try { const response = await fetch(qudosAjax.restUrl + 'share-strategy', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': qudosAjax.restNonce }, body: JSON.stringify({ strategy: anonymizedPlan }) }); if (response.ok) { showSuccessMessage('Strategy shared anonymously with the community!'); } else { throw new Error('Failed to share strategy'); } } catch (error) { console.error('Error sharing strategy:', error); showError('Unable to share strategy at this time. Please try again later.'); } } async function submitFeedback() { const feedback = document.getElementById('feedback-text')?.value.trim(); if (!feedback) { showError('Please enter your feedback before submitting.'); return; } try { const response = await fetch(qudosAjax.restUrl + 'feedback', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': qudosAjax.restNonce }, body: JSON.stringify({ feedback: feedback, timestamp: new Date().toISOString(), version: qudosAjax.version }) }); if (response.ok) { showSuccessMessage('Thank you for your feedback!'); document.getElementById('feedback-text').value = ''; } else { throw new Error('Failed to submit feedback'); } } catch (error) { console.error('Error submitting feedback:', error); showError('Unable to submit feedback at this time. Please try again later.'); } } // Utility functions for community features function getAgeRange(age) { if (age < 25) return 'under-25'; if (age < 35) return '25-34'; if (age < 45) return '35-44'; if (age < 55) return '45-54'; return '55-plus'; } function getSavingsRateCategory() { const income = parseFloat(document.getElementById('annual-income').value) || 0; const savings = parseFloat(document.getElementById('monthly-savings').value) * 12 || 0; const rate = income > 0 ? savings / income : 0; if (rate < 0.1) return 'low'; if (rate < 0.2) return 'moderate'; return 'high'; } function showSuccessMessage(message) { const successDiv = document.getElementById('success-display') || createSuccessDisplay(); const messageSpan = successDiv.querySelector('.success-message'); messageSpan.textContent = message; successDiv.style.display = 'block'; successDiv.setAttribute('role', 'status'); successDiv.setAttribute('aria-live', 'polite'); setTimeout(() => { successDiv.style.display = 'none'; }, 5000); } function createSuccessDisplay() { const successDiv = document.createElement('div'); successDiv.id = 'success-display'; successDiv.style = 'display: none; background: linear-gradient(135deg, #d4edda, #c3e6cb); color: #155724; padding: 20px; margin: 20px; border-radius: 12px; border-left: 5px solid #28a745; box-shadow: 0 4px 8px rgba(40, 167, 69, 0.1);'; successDiv.innerHTML = `
Success!
`; const errorDisplay = document.getElementById('error-display'); if (errorDisplay) { errorDisplay.parentNode.insertBefore(successDiv, errorDisplay.nextSibling); } return successDiv; } // Enhanced initialization with accessibility and performance document.addEventListener('DOMContentLoaded', function() { console.log('Qudos Ultimate Financial Planner v' + (qudosAjax.version || '5.2.0') + ' initializing...'); // Check for Chart.js availability if (typeof Chart === 'undefined') { console.error('Chart.js not loaded - charts will not be available'); } else { console.log('Chart.js loaded successfully'); } // Initialize accessibility features initializeAccessibilityFeatures(); // Load saved progress if available const savedProgress = loadProgress(); if (savedProgress) { // Restore form data restoreFormData(savedProgress.formData); // Navigate to saved step currentStep = savedProgress.currentStep; showStep(currentStep); console.log('Restored progress from step', currentStep); } else { // Start fresh showStep(1); } updateProgress(); updateTimelinePreview(); updateEconomicSettings(); updateCashFlow(); // Initialize Web Worker for background calculations setupWebWorker(); // Load theme preference const savedTheme = localStorage.getItem('qudos_theme'); if (savedTheme && savedTheme !== 'light') { toggleDarkMode(); } // Initialize performance monitoring if (window.performance) { const loadTime = window.performance.timing.loadEventEnd - window.performance.timing.navigationStart; console.log('Page load time:', loadTime + 'ms'); } console.log('Qudos Ultimate initialization complete'); }); function restoreFormData(formData) { if (!formData) return; // Restore basic form fields const fields = [ 'current-age', 'retirement-age', 'annual-income', 'annual-expenses', 'monthly-savings', 'current-savings', 'total-debt', 'debt-rate', 'tax-rate', 'inflation-rate', 'investment-return', 'salary-growth', 'emergency-target', 'has-partner', 'partner-age', 'partner-income', 'num-children' ]; fields.forEach(fieldId => { const element = document.getElementById(fieldId); const value = formData[fieldId.replace(/-/g, '')]; if (element && value !== undefined) { element.value = value; } }); // Restore goals if (formData.goals && Array.isArray(formData.goals)) { goals = [...formData.goals]; } // Trigger updates updateTimelinePreview(); updateEconomicSettings(); updateCashFlow(); if (goals.length > 0) { updateGoalsList(); } } // Initialize scenario analysis function initializeScenarioAnalysis(results) { // Set up scenario sliders with current values const incomeSlider = document.getElementById('income-change'); const marketSlider = document.getElementById('market-change'); if (incomeSlider) { incomeSlider.addEventListener('input', updateScenario); } if (marketSlider) { marketSlider.addEventListener('input', updateScenario); } // Initialize with current scenario updateScenario(); } // Performance optimization function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Debounced version of updateCashFlow for better performance const debouncedUpdateCashFlow = debounce(updateCashFlow, 300); // Add event listeners with debouncing where appropriate function addOptimizedEventListeners() { const numericInputs = document.querySelectorAll('input[type="number"]'); numericInputs.forEach(input => { input.addEventListener('input', debouncedUpdateCashFlow); }); const rangeInputs = document.querySelectorAll('input[type="range"]'); rangeInputs.forEach(input => { input.addEventListener('input', updateEconomicSettings); }); } '; /** * Get comprehensive inline CSS with accessibility and theming */ private function get_inline_css() { return ' /* Base styles and CSS custom properties */ :root { --primary-color: #667eea; --secondary-color: #764ba2; --success-color: #28a745; --warning-color: #ffc107; --danger-color: #dc3545; --info-color: #17a2b8; --light-color: #f8f9fa; --dark-color: #343a40; --font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; --border-radius: 12px; --box-shadow: 0 4px 8px rgba(0,0,0,0.1); --transition: all 0.3s ease; } /* Dark theme variables */ [data-theme="dark"] { --bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3a3a3a; --text-primary: #ffffff; --text-secondary: #cccccc; --text-muted: #999999; --border-color: #555555; } [data-theme="dark"] #qudos-ultimate { background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%); } [data-theme="dark"] #qudos-ultimate .wizard-step { background: var(--bg-secondary) !important; color: var(--text-primary) !important; } [data-theme="dark"] #qudos-ultimate input, [data-theme="dark"] #qudos-ultimate select, [data-theme="dark"] #qudos-ultimate textarea { background: var(--bg-tertiary) !important; color: var(--text-primary) !important; border-color: var(--border-color) !important; } /* High contrast mode for accessibility */ .high-contrast { --bg-primary: #000000; --bg-secondary: #000000; --text-primary: #ffffff; --text-secondary: #ffffff; --primary-color: #ffff00; --success-color: #00ff00; --danger-color: #ff0000; --warning-color: #ffff00; } .high-contrast * { border-color: #ffffff !important; } .high-contrast button { background: #ffff00 !important; color: #000000 !important; border: 2px solid #ffffff !important; } /* Focus styles for accessibility */ #qudos-ultimate *:focus { outline: 3px solid var(--primary-color) !important; outline-offset: 2px !important; } /* Enhanced button styles */ #qudos-ultimate button:hover { transform: translateY(-2px); filter: brightness(1.05); box-shadow: 0 6px 12px rgba(0,0,0,0.15); } #qudos-ultimate button:active { transform: translateY(0); } #qudos-ultimate button:disabled { opacity: 0.6; cursor: not-allowed; transform: none !important; } /* Input enhancements */ #qudos-ultimate input:focus, #qudos-ultimate select:focus, #qudos-ultimate textarea:focus { border-color: var(--primary-color) !important; box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15); transform: scale(1.02); } /* Step indicator enhancements */ .step-indicator.active { opacity: 1 !important; transform: scale(1.1); } .step-indicator.active div { background: white !important; color: var(--primary-color) !important; box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3); } /* Loading animation */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } @keyframes slideIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .wizard-step { animation: slideIn 0.5s ease-out; } /* Enhanced card styles */ .summary-card { transition: var(--transition); cursor: default; } .summary-card:hover { transform: translateY(-5px); box-shadow: 0 15px 35px rgba(0,0,0,0.1); } /* Goal item enhancements */ .goal-item:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); } .goal-item:focus { outline: 3px solid var(--primary-color); outline-offset: 2px; } /* Responsive design improvements */ @media (max-width: 768px) { #qudos-ultimate { margin: 10px; padding: 2px; } .wizard-step { padding: 20px !important; } div[style*="grid-template-columns"] { display: block !important; } div[style*="grid-template-columns"] > div { margin-bottom: 20px; } button { width: 100% !important; margin-bottom: 10px; min-height: 44px; } input, select, textarea { min-height: 44px; font-size: 16px; /* Prevents zoom on iOS */ } h1 { font-size: 1.8rem !important; } h2 { font-size: 1.5rem !important; } .step-indicator span { display: none; } } @media (max-width: 480px) { #qudos-ultimate { margin: 5px; } .wizard-step { padding: 15px !important; } h1 { font-size: 1.5rem !important; } } /* Print styles */ @media print { #qudos-ultimate { background: white !important; box-shadow: none !important; } .wizard-step[data-step="5"] { display: block !important; } .wizard-step:not([data-step="5"]) { display: none !important; } button { display: none !important; } } /* Screen reader only content */ .sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; } /* Reduced motion support */ @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* Language selector styles */ .language-selector { position: absolute; top: 20px; left: 20px; z-index: 10; } .language-selector select { background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 5px; padding: 5px 10px; font-size: 0.9rem; } /* Community features styles */ .community-section { margin-top: 30px; padding: 20px; background: linear-gradient(135deg, #f8f9fa, #e9ecef); border-radius: 15px; border: 1px solid #dee2e6; } .feedback-form textarea { width: 100%; min-height: 100px; padding: 15px; border: 2px solid #e9ecef; border-radius: 10px; font-family: var(--font-family); resize: vertical; } '; } // AJAX handler methods public function ajax_calculate() { if (!wp_verify_nonce($_POST['nonce'], 'qudos_nonce')) { wp_die('Security check failed'); } $data = $this->sanitize_input($_POST); $results = $this->calculate_comprehensive_projection($data); wp_send_json_success($results); } public function ajax_scenario() { if (!wp_verify_nonce($_POST['nonce'], 'qudos_nonce')) { wp_die('Security check failed'); } $data = $this->sanitize_input($_POST); $scenario_type = sanitize_text_field($_POST['scenario_type']); // Apply scenario modifications switch($scenario_type) { case 'increase_savings': $data['monthly_savings'] *= 1.5; break; case 'delay_retirement': $data['retirement_age'] += 2; break; case 'market_downturn': $data['investment_return'] -= 0.02; break; } $results = $this->calculate_comprehensive_projection($data); wp_send_json_success($results); } public function ajax_share_strategy() { if (!wp_verify_nonce($_POST['nonce'], 'qudos_nonce')) { wp_die('Security check failed'); } $strategy = json_decode(stripslashes($_POST['strategy']), true); if (!$strategy) { wp_send_json_error('Invalid strategy data'); return; } // Store anonymized strategy (implement based on requirements) $stored_id = $this->store_anonymized_strategy($strategy); if ($stored_id) { wp_send_json_success(['message' => 'Strategy shared successfully', 'id' => $stored_id]); } else { wp_send_json_error('Failed to share strategy'); } } public function ajax_submit_feedback() { if (!wp_verify_nonce($_POST['nonce'], 'qudos_nonce')) { wp_die('Security check failed'); } $feedback = sanitize_textarea_field($_POST['feedback']); if (empty($feedback)) { wp_send_json_error('Feedback cannot be empty'); return; } // Store feedback (implement based on requirements) $result = $this->store_feedback($feedback); if ($result) { wp_send_json_success(['message' => 'Feedback submitted successfully']); } else { wp_send_json_error('Failed to submit feedback'); } } // REST API handlers public function rest_share_strategy($request) { $strategy = $request->get_param('strategy'); if (!$this->validate_strategy_data($strategy)) { return new WP_Error('invalid_data', 'Invalid strategy data', ['status' => 400]); } $stored_id = $this->store_anonymized_strategy($strategy); if ($stored_id) { return new WP_REST_Response(['success' => true, 'id' => $stored_id], 200); } return new WP_Error('storage_failed', 'Failed to store strategy', ['status' => 500]); } public function rest_submit_feedback($request) { $feedback = sanitize_textarea_field($request->get_param('feedback')); if (empty($feedback)) { return new WP_Error('empty_feedback', 'Feedback cannot be empty', ['status' => 400]); } $result = $this->store_feedback($feedback); if ($result) { return new WP_REST_Response(['success' => true], 200); } return new WP_Error('storage_failed', 'Failed to store feedback', ['status' => 500]); } // Validation and helper methods public function validate_strategy_data($strategy) { if (!is_array($strategy)) return false; $required_fields = ['goals', 'demographics', 'strategy']; foreach ($required_fields as $field) { if (!isset($strategy[$field])) return false; } return true; } private function store_anonymized_strategy($strategy) { // Implement storage logic - could use custom post type, database table, etc. $post_id = wp_insert_post([ 'post_type' => 'qudos_strategy', 'post_status' => 'private', 'post_title' => 'Anonymized Strategy ' . date('Y-m-d H:i:s'), 'meta_input' => [ 'strategy_data' => json_encode($strategy), 'submission_date' => current_time('mysql'), 'ip_hash' => hash('sha256', $_SERVER['REMOTE_ADDR'] . 'qudos_salt') ] ]); return $post_id ? $post_id : false; } private function store_feedback($feedback) { // Implement feedback storage - could integrate with GitHub issues, email, etc. $post_id = wp_insert_post([ 'post_type' => 'qudos_feedback', 'post_status' => 'private', 'post_title' => 'User Feedback ' . date('Y-m-d H:i:s'), 'post_content' => $feedback, 'meta_input' => [ 'submission_date' => current_time('mysql'), 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'version' => self::VERSION ] ]); return $post_id ? $post_id : false; } private function sanitize_input($input) { $defaults = [ 'current_age' => 30, 'retirement_age' => 65, 'annual_income' => 0, 'annual_expenses' => 0, 'monthly_savings' => 0, 'current_savings' => 0, 'total_debt' => 0, 'debt_rate' => 0, 'tax_rate' => 25, 'inflation_rate' => 2.9, 'salary_growth' => 3.9, 'investment_return' => 6.5, 'emergency_target' => 6, 'has_partner' => false, 'partner_income' => 0, 'num_children' => 0, 'goals' => '[]' ]; $sanitized = []; foreach ($defaults as $key => $default) { $value = isset($input[$key]) ? $input[$key] : $default; switch ($key) { case 'current_age': case 'retirement_age': case 'num_children': case 'emergency_target': $sanitized[$key] = intval($value); break; case 'has_partner': $sanitized[$key] = filter_var($value, FILTER_VALIDATE_BOOLEAN); break; case 'goals': $sanitized[$key] = $this->sanitize_goals($value); break; default: $sanitized[$key] = floatval($value); break; } } // Convert percentages foreach (['debt_rate', 'tax_rate', 'inflation_rate', 'salary_growth', 'investment_return'] as $rate_field) { if (isset($sanitized[$rate_field])) { $sanitized[$rate_field] = $sanitized[$rate_field] / 100; } } return $sanitized; } private function sanitize_goals($goals_json) { $goals = json_decode(stripslashes($goals_json), true); if (!is_array($goals)) return []; $sanitized = []; foreach ($goals as $goal) { if (isset($goal['name']) && isset($goal['cost']) && isset($goal['age'])) { $sanitized[] = [ 'id' => intval($goal['id'] ?? time()), 'name' => sanitize_text_field($goal['name']), 'cost' => floatval($goal['cost']), 'age' => intval($goal['age']), 'icon' => sanitize_text_field($goal['icon'] ?? '🎯'), 'category' => sanitize_text_field($goal['category'] ?? 'custom'), 'priority' => intval($goal['priority'] ?? 1) ]; } } return $sanitized; } // Monte Carlo and calculation methods would continue here... // For brevity, including the main render method public function render_shortcode($atts) { ob_start(); include plugin_dir_path(__FILE__) . 'templates/qudos-ultimate-template.php'; return ob_get_clean(); } } // Initialize the plugin new QudosUltimatePlanner(); // End of Block 6 - Complete plugin code ready for deployment ?> QUDOS COIN - TESTER

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

en_USEnglish