/** * ROI Calculator Application * Main application class that coordinates all modules */ class ROICalculatorApp { constructor() { this.calculator = null; this.chartManager = null; this.uiManager = null; this.exportManager = null; this.isInitialized = false; } async initialize() { if (this.isInitialized) { console.warn('ROI Calculator already initialized'); return; } try { // Create the main calculator instance this.calculator = new ROICalculator(); // Create UI and chart managers this.uiManager = new UIManager(this.calculator); this.chartManager = new ChartManager(this.calculator); this.exportManager = new ExportManager(this.calculator, this.uiManager); // Replace the methods in calculator with manager methods this.calculator.updateSummaryMetrics = () => this.uiManager.updateSummaryMetrics(); this.calculator.updateCharts = () => this.chartManager.updateCharts(); this.calculator.updateComparisonTable = () => this.uiManager.updateComparisonTable(); this.calculator.updateMonthlyBreakdown = () => this.uiManager.updateMonthlyBreakdown(); this.calculator.initializeCharts = () => this.chartManager.initializeCharts(); this.calculator.formatCurrency = (amount) => this.uiManager.formatCurrency(amount); this.calculator.formatCurrencyDetailed = (amount) => this.uiManager.formatCurrencyDetailed(amount); this.calculator.formatPercentage = (value) => this.uiManager.formatPercentage(value); // Re-initialize charts with the chart manager this.chartManager.initializeCharts(); this.calculator.charts = this.chartManager.charts; // Initialize tooltips this.initializeTooltips(); // Check export libraries this.checkExportLibraries(); // Run initial calculation this.calculator.updateCalculations(); this.isInitialized = true; console.log('ROI Calculator initialized successfully'); } catch (error) { console.error('Failed to initialize ROI Calculator:', error); } } // Initialize tooltips with Bootstrap (loaded directly via CDN) initializeTooltips() { // Wait for Bootstrap to be available if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { try { var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); } catch (error) { console.warn('Failed to initialize Bootstrap tooltips:', error); this.initializeNativeTooltips(); } } else { // Retry after a short delay for deferred scripts setTimeout(() => this.initializeTooltips(), 100); } } initializeNativeTooltips() { try { var tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]'); tooltipElements.forEach(function (element) { // Ensure the title attribute is set for native tooltips var tooltipContent = element.getAttribute('title'); if (!tooltipContent) { // Get tooltip content from data-bs-original-title or title attribute tooltipContent = element.getAttribute('data-bs-original-title'); if (tooltipContent) { element.setAttribute('title', tooltipContent); } } }); } catch (error) { console.error('Error initializing native tooltips:', error); } } // Check if export libraries are loaded checkExportLibraries() { try { const pdfButton = document.querySelector('button[onclick="exportToPDF()"]'); if (typeof window.jspdf === 'undefined') { if (pdfButton) { pdfButton.disabled = true; pdfButton.innerHTML = ' Loading PDF...'; } // Retry after a short delay setTimeout(() => { if (typeof window.jspdf !== 'undefined' && pdfButton) { pdfButton.disabled = false; pdfButton.innerHTML = ' Export PDF Report'; } }, 2000); } } catch (error) { console.error('Error checking export libraries:', error); } } // Public API methods for global function calls updateCalculations() { if (this.calculator) { this.calculator.updateCalculations(); this.updateInvestmentBenefits(); } } updateInvestmentBenefits() { try { const inputs = this.calculator.getInputValues(); const investmentAmount = inputs.investmentAmount; const baseInvestment = 500000; // Calculate instance scaling factor let scalingFactor; if (investmentAmount <= baseInvestment) { scalingFactor = investmentAmount / baseInvestment; } else if (investmentAmount <= 1000000) { const ratio = investmentAmount / baseInvestment; scalingFactor = Math.pow(ratio, 1.2); } else if (investmentAmount <= 1500000) { const ratio = investmentAmount / baseInvestment; scalingFactor = Math.pow(ratio, 1.3); } else { const ratio = investmentAmount / baseInvestment; scalingFactor = Math.pow(ratio, 1.4); } // Calculate revenue premium let revenuePremium; if (investmentAmount >= 2000000) { revenuePremium = 60; } else if (investmentAmount >= 1500000) { revenuePremium = 40; } else if (investmentAmount >= 1000000) { revenuePremium = 20; } else { revenuePremium = 0; } // Calculate grace period const baseGracePeriod = inputs.gracePeriod; let gracePeriodBonus; if (investmentAmount >= 1500000) { gracePeriodBonus = 12; } else if (investmentAmount >= 1000000) { gracePeriodBonus = 6; } else { gracePeriodBonus = Math.floor((investmentAmount - baseInvestment) / 250000); } const totalGracePeriod = Math.max(0, baseGracePeriod + gracePeriodBonus); // Calculate max performance bonus let maxBonus; if (investmentAmount >= 2000000) { maxBonus = 35; } else if (investmentAmount >= 1500000) { maxBonus = 25; } else if (investmentAmount >= 1000000) { maxBonus = 20; } else { maxBonus = 15; } // Update UI elements const instanceScaling = document.getElementById('instance-scaling'); if (instanceScaling) { instanceScaling.textContent = scalingFactor.toFixed(1) + 'x'; instanceScaling.className = scalingFactor >= 2.0 ? 'benefit-value text-success fw-bold' : scalingFactor >= 1.5 ? 'benefit-value text-primary fw-bold' : 'benefit-value text-secondary fw-bold'; } const revenuePremiumEl = document.getElementById('revenue-premium'); if (revenuePremiumEl) { revenuePremiumEl.textContent = '+' + revenuePremium + '%'; revenuePremiumEl.className = revenuePremium >= 40 ? 'benefit-value text-success fw-bold' : revenuePremium >= 20 ? 'benefit-value text-warning fw-bold' : 'benefit-value text-secondary fw-bold'; } const gracePeriodEl = document.getElementById('grace-period-display'); if (gracePeriodEl) { gracePeriodEl.textContent = totalGracePeriod + ' months'; gracePeriodEl.className = totalGracePeriod >= 12 ? 'benefit-value text-success fw-bold' : totalGracePeriod >= 9 ? 'benefit-value text-info fw-bold' : 'benefit-value text-secondary fw-bold'; } const maxBonusEl = document.getElementById('max-bonus'); if (maxBonusEl) { maxBonusEl.textContent = maxBonus + '%'; maxBonusEl.className = maxBonus >= 30 ? 'benefit-value text-success fw-bold' : maxBonus >= 20 ? 'benefit-value text-warning fw-bold' : 'benefit-value text-secondary fw-bold'; } } catch (error) { console.error('Error updating investment benefits:', error); } } exportToPDF() { if (this.exportManager) { this.exportManager.exportToPDF(); } } exportToCSV() { if (this.exportManager) { this.exportManager.exportToCSV(); } } updateInvestmentAmount(value) { try { const input = document.getElementById('investment-amount'); if (input) { input.setAttribute('data-value', value); input.value = InputUtils.formatNumberWithCommas(value); this.updateCalculations(); } } catch (error) { console.error('Error updating investment amount:', error); } } updateRevenuePerInstance(value) { try { const element = document.getElementById('revenue-per-instance'); if (element) { element.value = value; this.updateCalculations(); } } catch (error) { console.error('Error updating revenue per instance:', error); } } updateServalaShare(value) { try { const element = document.getElementById('servala-share'); if (element) { element.value = value; this.updateCalculations(); } } catch (error) { console.error('Error updating servala share:', error); } } updateGracePeriod(value) { try { const element = document.getElementById('grace-period'); if (element) { element.value = value; this.updateCalculations(); } } catch (error) { console.error('Error updating grace period:', error); } } updateLoanRate(value) { try { const element = document.getElementById('loan-interest-rate'); if (element) { element.value = value; this.updateCalculations(); } } catch (error) { console.error('Error updating loan rate:', error); } } updateCoreServiceRevenue(value) { try { const element = document.getElementById('core-service-revenue'); if (element) { element.value = value; this.updateCalculations(); } } catch (error) { console.error('Error updating core service revenue:', error); } } updateCurrency(value) { try { const currencyElement = document.getElementById('currency'); if (currencyElement) { currencyElement.value = value; } // Update all currency-related UI labels this.updateCurrencyLabels(value); // Update calculations to reflect new currency formatting this.updateCalculations(); } catch (error) { console.error('Error updating currency:', error); } } updateCurrencyLabels(currency) { try { // Update investment amount prefix const investmentPrefix = document.getElementById('investment-currency-prefix'); if (investmentPrefix) { investmentPrefix.textContent = currency; } // Update investment min/max labels const investmentMinLabel = document.getElementById('investment-min-label'); if (investmentMinLabel) { investmentMinLabel.textContent = `${currency} 100K`; } const investmentMaxLabel = document.getElementById('investment-max-label'); if (investmentMaxLabel) { investmentMaxLabel.textContent = `${currency} 2M`; } // Update revenue per instance suffix const revenueSuffix = document.getElementById('revenue-currency-suffix'); if (revenueSuffix) { revenueSuffix.textContent = `${currency}/month`; } // Update core service revenue suffix (it's a direct span, not ID-based) const coreRevenueInput = document.getElementById('core-service-revenue'); if (coreRevenueInput) { const coreRevenueSpan = coreRevenueInput.parentElement.querySelector('.input-group-text'); if (coreRevenueSpan) { coreRevenueSpan.textContent = `${currency}/month`; } } // Update all other currency labels throughout the interface const currencyLabels = document.querySelectorAll('.currency-label'); currencyLabels.forEach(label => { label.textContent = currency; }); // Update range slider labels with currency const revenueLabel = document.querySelector('label[for="revenue-per-instance"]'); if (revenueLabel) { revenueLabel.innerHTML = revenueLabel.innerHTML.replace(/(CHF|EUR)/, currency); } const coreRevenueLabel = document.querySelector('label[for="core-service-revenue"]'); if (coreRevenueLabel) { coreRevenueLabel.innerHTML = coreRevenueLabel.innerHTML.replace(/(CHF|EUR)/, currency); } // Update investment amount field display if it has a value const investmentInput = document.getElementById('investment-amount'); if (investmentInput && investmentInput.getAttribute('data-value')) { const currentValue = investmentInput.getAttribute('data-value'); investmentInput.value = InputUtils.formatNumberWithCommas(currentValue, currency); } } catch (error) { console.error('Error updating currency labels:', error); } } updateScenarioChurn(scenarioKey, churnRate) { try { if (this.calculator && this.calculator.scenarios[scenarioKey]) { this.calculator.scenarios[scenarioKey].churnRate = parseFloat(churnRate) / 100; this.updateCalculations(); } } catch (error) { console.error('Error updating scenario churn:', error); } } updateScenarioPhase(scenarioKey, phaseIndex, newInstancesPerMonth) { try { if (this.calculator && this.calculator.scenarios[scenarioKey] && this.calculator.scenarios[scenarioKey].phases[phaseIndex]) { this.calculator.scenarios[scenarioKey].phases[phaseIndex].newInstancesPerMonth = parseInt(newInstancesPerMonth); this.updateCalculations(); } } catch (error) { console.error('Error updating scenario phase:', error); } } resetAdvancedParameters() { if (!confirm('Reset all advanced parameters to default values?')) { return; } try { if (!this.calculator) return; // Reset Conservative this.calculator.scenarios.conservative.churnRate = 0.02; this.calculator.scenarios.conservative.phases = [ { months: 6, newInstancesPerMonth: 50 }, { months: 6, newInstancesPerMonth: 75 }, { months: 12, newInstancesPerMonth: 100 }, { months: 12, newInstancesPerMonth: 150 } ]; // Reset Moderate this.calculator.scenarios.moderate.churnRate = 0.03; this.calculator.scenarios.moderate.phases = [ { months: 6, newInstancesPerMonth: 100 }, { months: 6, newInstancesPerMonth: 200 }, { months: 12, newInstancesPerMonth: 300 }, { months: 12, newInstancesPerMonth: 400 } ]; // Reset Aggressive this.calculator.scenarios.aggressive.churnRate = 0.05; this.calculator.scenarios.aggressive.phases = [ { months: 6, newInstancesPerMonth: 200 }, { months: 6, newInstancesPerMonth: 400 }, { months: 12, newInstancesPerMonth: 600 }, { months: 12, newInstancesPerMonth: 800 } ]; // Update UI inputs const inputMappings = [ ['conservative-churn', '2.0'], ['conservative-phase-0', '50'], ['conservative-phase-1', '75'], ['conservative-phase-2', '100'], ['conservative-phase-3', '150'], ['moderate-churn', '3.0'], ['moderate-phase-0', '100'], ['moderate-phase-1', '200'], ['moderate-phase-2', '300'], ['moderate-phase-3', '400'], ['aggressive-churn', '5.0'], ['aggressive-phase-0', '200'], ['aggressive-phase-1', '400'], ['aggressive-phase-2', '600'], ['aggressive-phase-3', '800'] ]; inputMappings.forEach(([id, value]) => { const element = document.getElementById(id); if (element) { element.value = value; } }); this.updateCalculations(); } catch (error) { console.error('Error resetting advanced parameters:', error); } } toggleScenario(scenarioKey) { try { const checkbox = document.getElementById(scenarioKey + '-enabled'); if (!checkbox || !this.calculator) return; const enabled = checkbox.checked; this.calculator.scenarios[scenarioKey].enabled = enabled; const card = document.getElementById(scenarioKey + '-card'); if (card) { if (enabled) { card.classList.add('active'); card.classList.remove('disabled'); } else { card.classList.remove('active'); card.classList.add('disabled'); } } this.updateCalculations(); } catch (error) { console.error('Error toggling scenario:', error); } } toggleCollapsible(elementId) { try { const content = document.getElementById(elementId); if (!content) return; const header = content.previousElementSibling; if (!header) return; const chevron = header.querySelector('.bi-chevron-down, .bi-chevron-up'); if (content.classList.contains('show')) { content.classList.remove('show'); if (chevron) { chevron.classList.remove('bi-chevron-up'); chevron.classList.add('bi-chevron-down'); } } else { content.classList.add('show'); if (chevron) { chevron.classList.remove('bi-chevron-down'); chevron.classList.add('bi-chevron-up'); } } } catch (error) { console.error('Error toggling collapsible:', error); } } resetCalculator() { if (!confirm('Are you sure you want to reset all parameters to default values?')) { return; } try { // Reset input values const investmentInput = document.getElementById('investment-amount'); if (investmentInput) { investmentInput.setAttribute('data-value', '500000'); investmentInput.value = '500,000'; } const resetMappings = [ ['investment-slider', 500000], ['timeframe', 3], ['loan-interest-rate', 5.0], ['loan-rate-slider', 5.0], ['revenue-per-instance', 50], ['revenue-slider', 50], ['servala-share', 25], ['share-slider', 25], ['grace-period', 6], ['grace-slider', 6] ]; resetMappings.forEach(([id, value]) => { const element = document.getElementById(id); if (element) { element.value = value; } }); // Both models are now calculated simultaneously // Reset scenarios ['conservative', 'moderate', 'aggressive'].forEach(scenario => { const checkbox = document.getElementById(scenario + '-enabled'); const card = document.getElementById(scenario + '-card'); if (checkbox) checkbox.checked = true; if (this.calculator) this.calculator.scenarios[scenario].enabled = true; if (card) { card.classList.add('active'); card.classList.remove('disabled'); } }); // Reset advanced parameters this.resetAdvancedParameters(); // Both models are now calculated simultaneously, no toggle needed // Recalculate this.updateCalculations(); } catch (error) { console.error('Error resetting calculator:', error); } } // toggleInvestmentModel removed - both models are now calculated simultaneously updateMonthlyBreakdownFilters() { try { if (this.uiManager) { this.uiManager.updateMonthlyBreakdown(); } } catch (error) { console.error('Error updating monthly breakdown filters:', error); } } logout() { if (!confirm('Are you sure you want to logout?')) { return; } try { // Create a form to submit logout request const form = document.createElement('form'); form.method = 'POST'; form.action = window.location.pathname; // Add CSRF token from page meta tag or cookie const csrfInput = document.createElement('input'); csrfInput.type = 'hidden'; csrfInput.name = 'csrfmiddlewaretoken'; csrfInput.value = InputUtils.getCSRFToken(); form.appendChild(csrfInput); // Add logout parameter const logoutInput = document.createElement('input'); logoutInput.type = 'hidden'; logoutInput.name = 'logout'; logoutInput.value = 'true'; form.appendChild(logoutInput); document.body.appendChild(form); form.submit(); } catch (error) { console.error('Error during logout:', error); } } } // Initialize the application when DOM is ready document.addEventListener('DOMContentLoaded', function () { window.ROICalculatorApp = new ROICalculatorApp(); window.ROICalculatorApp.initialize(); });