// 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 = `
`;
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
Leave a Reply