diff --git a/hub/services/static/css/roi-calculator.css b/hub/services/static/css/roi-calculator.css index f23c064..6626526 100644 --- a/hub/services/static/css/roi-calculator.css +++ b/hub/services/static/css/roi-calculator.css @@ -101,14 +101,109 @@ .chart-container { position: relative; - height: 400px; background: white; border-radius: 8px; - padding: 1rem; + padding: 1.5rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin-bottom: 1.5rem; } +/* Enhanced chart sizing for new layout */ +.chart-container canvas { + max-height: 400px; +} + +/* Full-width chart containers */ +.card-body canvas { + width: 100% !important; +} + +/* Primary chart gets extra height */ +#instanceGrowthChart { + height: 500px !important; +} + +/* Secondary charts get good height */ +#revenueChart, #cashFlowChart, #modelComparisonChart { + height: 400px !important; +} + +/* Enhanced layout styles for new design */ +.sticky-top { + z-index: 1020; +} + +.card { + transition: box-shadow 0.15s ease-in-out; +} + +.card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +/* Compact header controls */ +.form-range { + height: 4px; +} + +.input-group-sm .form-control, +.form-select-sm, +.btn-sm { + font-size: 0.825rem; +} + +/* Clean chart headers */ +.card-header { + background: white !important; + border: none !important; + padding-bottom: 0.5rem; +} + +.card-body { + padding: 1.5rem; +} + +/* Responsive chart heights */ +@media (max-width: 768px) { + #instanceGrowthChart { + height: 350px !important; + } + + #revenueChart, #cashFlowChart, #modelComparisonChart { + height: 300px !important; + } + + .card-body { + padding: 1rem; + } +} + +@media (max-width: 576px) { + #instanceGrowthChart { + height: 250px !important; + } + + #revenueChart, #cashFlowChart, #modelComparisonChart { + height: 200px !important; + } +} + +/* Manual collapse functionality */ +.collapse { + display: none; +} + +.collapse.show { + display: block; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + transition: height 0.35s ease; +} + .export-buttons { position: sticky; top: 20px; diff --git a/hub/services/static/js/roi-calculator/chart-manager.js b/hub/services/static/js/roi-calculator/chart-manager.js index 8e5834f..0e8d7e6 100644 --- a/hub/services/static/js/roi-calculator/chart-manager.js +++ b/hub/services/static/js/roi-calculator/chart-manager.js @@ -84,7 +84,7 @@ class ChartManager { }); // Model Comparison Chart (replaces generic Cash Flow Chart) - const modelComparisonCanvas = document.getElementById('cashFlowChart'); + const modelComparisonCanvas = document.getElementById('modelComparisonChart'); if (!modelComparisonCanvas) { console.error('Model comparison chart canvas not found'); return; @@ -98,7 +98,7 @@ class ChartManager { maintainAspectRatio: false, plugins: { legend: { position: 'top' }, - title: { display: true, text: 'Investment Model Performance Comparison' } + title: { display: true, text: 'Investment Model Comparison' } }, scales: { y: { @@ -111,6 +111,35 @@ class ChartManager { } } }); + + // Performance Comparison Chart (for cashFlowChart canvas) + const performanceCanvas = document.getElementById('cashFlowChart'); + if (!performanceCanvas) { + console.error('Performance comparison chart canvas not found'); + return; + } + const performanceCtx = performanceCanvas.getContext('2d'); + this.charts.performance = new Chart(performanceCtx, { + type: 'bar', + data: { labels: [], datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' }, + title: { display: true, text: 'Model Performance Comparison' } + }, + scales: { + y: { + beginAtZero: true, + title: { display: true, text: 'ROI (%)' } + }, + x: { + title: { display: true, text: 'Growth Scenario' } + } + } + } + }); } catch (error) { console.error('Error initializing charts:', error); this.showChartError('Failed to initialize charts. Please refresh the page.'); @@ -119,7 +148,7 @@ class ChartManager { showChartError(message) { // Show error message in place of charts - const chartContainers = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart']; + const chartContainers = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart', 'modelComparisonChart']; chartContainers.forEach(containerId => { const container = document.getElementById(containerId); if (container) { @@ -152,7 +181,7 @@ class ChartManager { // Update ROI Progression Chart this.charts.roiProgression.data.labels = monthLabels; - this.charts.roiProgression.data.datasets = scenarios.map(scenario => ({ + this.charts.roiProgression.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({ label: `${this.calculator.scenarios[scenario].name} (${this.calculator.results[scenario].investmentModel})`, data: this.calculator.monthlyData[scenario].map(d => d.roiPercent), borderColor: colors[scenario], @@ -166,7 +195,7 @@ class ChartManager { // Update Net Position Chart (Break-Even Analysis) this.charts.netPosition.data.labels = monthLabels; - this.charts.netPosition.data.datasets = scenarios.map(scenario => ({ + this.charts.netPosition.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({ label: `${this.calculator.scenarios[scenario].name} Net Position`, data: this.calculator.monthlyData[scenario].map(d => d.netPosition), borderColor: colors[scenario], @@ -180,37 +209,122 @@ class ChartManager { })); this.charts.netPosition.update(); - // Update Model Comparison Chart - const scenarioLabels = scenarios.map(s => this.calculator.scenarios[s].name); - const currentInvestmentModel = Object.values(this.calculator.results)[0]?.investmentModel || 'direct'; + // Update Model Comparison Chart - Side-by-side comparison of both models + const inputs = this.calculator.getInputValues(); - // Show comparison with both models for the same scenarios - this.charts.modelComparison.data.labels = scenarioLabels; - this.charts.modelComparison.data.datasets = [{ - label: `${currentInvestmentModel === 'loan' ? 'Loan Model' : 'Direct Investment'} - Final Return`, - data: scenarios.map(scenario => this.calculator.results[scenario].cspRevenue), - backgroundColor: scenarios.map(scenario => colors[scenario] + '80'), - borderColor: scenarios.map(scenario => colors[scenario]), - borderWidth: 2 - }]; + // Calculate loan model net profit (fixed return regardless of scenario) + const loanMonthlyPayment = this.calculateLoanPayment(inputs.investmentAmount, inputs.loanInterestRate, inputs.timeframe); + const loanTotalPayments = loanMonthlyPayment * (inputs.timeframe * 12); + const loanNetProfit = loanTotalPayments - inputs.investmentAmount; - // Add performance metrics for direct investment - if (currentInvestmentModel === 'direct') { - this.charts.modelComparison.data.datasets.push({ - label: 'Performance Bonus Impact', - data: scenarios.map(scenario => - this.calculator.results[scenario].avgPerformanceBonus * - this.calculator.results[scenario].cspRevenue - ), - backgroundColor: '#17a2b8', - borderColor: '#17a2b8', + // Prepare scenario-based comparison + const enabledScenarios = scenarios.filter(s => this.calculator.scenarios[s].enabled); + const comparisonLabels = enabledScenarios.map(s => this.calculator.scenarios[s].name); + + // Loan model data (same profit for all scenarios since it's fixed) + const loanModelData = enabledScenarios.map(() => loanNetProfit); + + // Direct investment data (varies by scenario performance) + const directInvestmentData = enabledScenarios.map(scenario => { + const scenarioResult = this.calculator.results[scenario]; + if (!scenarioResult) return 0; + return scenarioResult.cspRevenue - inputs.investmentAmount; + }); + + // Performance bonus data (shows the additional revenue from performance bonuses) + const performanceBonusData = enabledScenarios.map(scenario => { + const monthlyData = this.calculator.monthlyData[scenario] || []; + const totalPerformanceBonus = monthlyData.reduce((sum, month) => { + // Calculate the bonus revenue (difference from standard share) + const standardRevenue = month.monthlyRevenue * inputs.servalaShare; + const actualServalaRevenue = month.servalaRevenue; + const bonusAmount = Math.max(0, standardRevenue - actualServalaRevenue); // CSP gets this as bonus + return sum + bonusAmount; + }, 0); + return totalPerformanceBonus; + }); + + this.charts.modelComparison.data.labels = comparisonLabels; + this.charts.modelComparison.data.datasets = [ + { + label: `Loan Model (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed return)`, + data: loanModelData, + backgroundColor: '#ffc107', + borderColor: '#e0a800', borderWidth: 2 - }); - } + }, + { + label: 'Direct Investment (base return)', + data: directInvestmentData, + backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '80'), + borderColor: enabledScenarios.map(scenario => colors[scenario]), + borderWidth: 2 + }, + { + label: 'Performance Bonus Impact', + data: performanceBonusData, + backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '40'), + borderColor: enabledScenarios.map(scenario => colors[scenario]), + borderWidth: 2, + borderDash: [5, 5] + } + ]; this.charts.modelComparison.update(); + + // Update Performance Comparison Chart (ROI comparison for both models) + this.charts.performance.data.labels = comparisonLabels; + + // Calculate ROI for loan model (same for all scenarios) + const loanROI = (loanNetProfit / inputs.investmentAmount) * 100; + const loanROIData = enabledScenarios.map(() => loanROI); + + // Get ROI for direct investment (varies by scenario) + const directROIData = enabledScenarios.map(scenario => { + const result = this.calculator.results[scenario]; + return result ? result.roi : 0; + }); + + this.charts.performance.data.datasets = [ + { + label: `Loan Model ROI (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed)`, + data: loanROIData, + backgroundColor: '#ffc107', + borderColor: '#e0a800', + borderWidth: 2 + }, + { + label: 'Direct Investment ROI', + data: directROIData, + backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '80'), + borderColor: enabledScenarios.map(scenario => colors[scenario]), + borderWidth: 2 + } + ]; + + this.charts.performance.update(); } catch (error) { console.error('Error updating charts:', error); } } + + // Helper method to calculate loan payment + calculateLoanPayment(principal, annualRate, years) { + try { + const monthlyRate = annualRate / 12; + const numberOfPayments = years * 12; + + if (monthlyRate === 0) { + return principal / numberOfPayments; + } + + const monthlyPayment = principal * (monthlyRate * Math.pow(1 + monthlyRate, numberOfPayments)) / + (Math.pow(1 + monthlyRate, numberOfPayments) - 1); + + return monthlyPayment; + } catch (error) { + console.error('Error calculating loan payment:', error); + return 0; + } + } } \ No newline at end of file diff --git a/hub/services/templates/calculator/csp_roi_calculator.html b/hub/services/templates/calculator/csp_roi_calculator.html index d221826..31622d8 100644 --- a/hub/services/templates/calculator/csp_roi_calculator.html +++ b/hub/services/templates/calculator/csp_roi_calculator.html @@ -40,449 +40,275 @@ function toggleCollapsible(elementId) { window.ROICalculatorApp?.toggleCollapsib function resetCalculator() { window.ROICalculatorApp?.resetCalculator(); } function toggleInvestmentModel() { window.ROICalculatorApp?.toggleInvestmentModel(); } function logout() { window.ROICalculatorApp?.logout(); } + +// Manual toggle functions for collapse elements +function toggleAdvancedControls() { + const element = document.getElementById('advancedControls'); + const button = document.getElementById('advancedToggleBtn'); + + console.log('Toggling advanced controls, current classes:', element.className); + + if (element.style.display === 'none' || element.style.display === '') { + element.style.display = 'block'; + button.innerHTML = ' Less'; + console.log('Showing advanced controls'); + } else { + element.style.display = 'none'; + button.innerHTML = ' More'; + console.log('Hiding advanced controls'); + } +} + +function toggleDataCollapse() { + const element = document.getElementById('dataCollapse'); + const button = document.getElementById('dataToggleBtn'); + + console.log('Toggling data collapse, current style:', element.style.display); + + if (element.style.display === 'none' || element.style.display === '') { + element.style.display = 'block'; + button.classList.remove('collapsed'); + button.setAttribute('aria-expanded', 'true'); + console.log('Showing data collapse'); + } else { + element.style.display = 'none'; + button.classList.add('collapsed'); + button.setAttribute('aria-expanded', 'false'); + console.log('Hiding data collapse'); + } +} + +// Initialize collapse states +document.addEventListener('DOMContentLoaded', function() { + // Ensure both sections start collapsed + const advancedControls = document.getElementById('advancedControls'); + const dataCollapse = document.getElementById('dataCollapse'); + + if (advancedControls) { + advancedControls.style.display = 'none'; + } + if (dataCollapse) { + dataCollapse.style.display = 'none'; + } + + console.log('Collapse elements initialized as hidden'); +}); {% endblock %} {% block content %} -
Calculate potential returns from investing in Servala platform
+This ROI calculator models your investment in the Servala platform with two distinct approaches. Choose between guaranteed fixed returns through lending, or performance-based returns through direct investment that rewards your sales capabilities.
- -Investment Model: Loan (3-7% fixed returns) vs Direct Investment (15-40% performance-based returns).
-Investment Amount: Higher amounts unlock progressive scaling benefits - 500k = 1.0x, 1M = 1.5x, 2M = 2.0x performance.
-Dynamic Grace Period: Larger investments get longer 100% revenue retention (6+ months based on investment size).
-Performance Bonuses: Direct Investment CSPs earn up to 15% additional revenue share for exceeding sales targets.
-Monthly Revenue per Instance: Recurring revenue from managed services (typically CHF 50-200 per instance).
-Loan Model: Lend capital at fixed interest (3-7% annually). Predictable monthly payments, lower risk, but capped returns.
-Direct Investment: Invest in operations with revenue sharing. Progressive scaling, performance bonuses, and extended grace periods reward larger investments and active sales participation.
-Conservative: Steady growth with low churn (2%), suitable for established markets.
-Moderate: Balanced growth with moderate churn (3%), typical for competitive markets.
-Aggressive: Rapid expansion with higher churn (5%), for high-growth strategies.
-Each scenario has 4 growth phases with customizable instance acquisition rates in Advanced Parameters.
-Net Position: Your financial position after accounting for initial investment (above zero = profitable).
-ROI Progression: How your returns develop over time - shows when investment becomes profitable.
-Performance Multiplier: How much your actual results exceed baseline expectations (1.0x = baseline, 1.5x = 50% better).
-Model Comparison: Direct side-by-side comparison of loan vs direct investment returns for your specific scenario.
-Investment Scaling: Larger investments unlock operational advantages - better customer acquisition and retention capabilities.
-How Investment Amount Affects Growth
-Higher investments enable better growth through increased marketing, infrastructure, and customer success capabilities. This affects instance acquisition rates and reduces churn.
- -Mathematical Impact
-Example: CHF 1M investment = 1.41× more instances + 25% lower churn than CHF 500K base.
-