From 7d94501858b4adb53037ceb67e82209262dec9b0 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Mon, 21 Jul 2025 15:56:21 +0200 Subject: [PATCH] move roi calc js into own file --- hub/services/static/js/roi-calculator.js | 956 +++++++++++++++++ .../calculator/csp_roi_calculator.html | 960 +----------------- 2 files changed, 957 insertions(+), 959 deletions(-) create mode 100644 hub/services/static/js/roi-calculator.js diff --git a/hub/services/static/js/roi-calculator.js b/hub/services/static/js/roi-calculator.js new file mode 100644 index 0000000..d0fc328 --- /dev/null +++ b/hub/services/static/js/roi-calculator.js @@ -0,0 +1,956 @@ +class ROICalculator { + constructor() { + this.scenarios = { + conservative: { + name: 'Conservative', + enabled: true, + churnRate: 0.02, + phases: [ + { months: 6, newInstancesPerMonth: 50 }, + { months: 6, newInstancesPerMonth: 75 }, + { months: 12, newInstancesPerMonth: 100 }, + { months: 12, newInstancesPerMonth: 150 } + ] + }, + moderate: { + name: 'Moderate', + enabled: true, + churnRate: 0.03, + phases: [ + { months: 6, newInstancesPerMonth: 100 }, + { months: 6, newInstancesPerMonth: 200 }, + { months: 12, newInstancesPerMonth: 300 }, + { months: 12, newInstancesPerMonth: 400 } + ] + }, + aggressive: { + name: 'Aggressive', + enabled: true, + churnRate: 0.05, + phases: [ + { months: 6, newInstancesPerMonth: 200 }, + { months: 6, newInstancesPerMonth: 400 }, + { months: 12, newInstancesPerMonth: 600 }, + { months: 12, newInstancesPerMonth: 800 } + ] + } + }; + + this.charts = {}; + this.monthlyData = {}; + this.results = {}; + + // Initialize charts + this.initializeCharts(); + + // Initial calculation + this.updateCalculations(); + } + + getInputValues() { + return { + investmentAmount: parseFloat(document.getElementById('investment-amount').getAttribute('data-value')), + timeframe: parseInt(document.getElementById('timeframe').value), + discountRate: parseFloat(document.getElementById('discount-rate').value) / 100, + revenuePerInstance: parseFloat(document.getElementById('revenue-per-instance').value), + servalaShare: parseFloat(document.getElementById('servala-share').value) / 100, + gracePeriod: parseInt(document.getElementById('grace-period').value) + }; + } + + calculateScenario(scenarioKey, inputs) { + const scenario = this.scenarios[scenarioKey]; + if (!scenario.enabled) return null; + + // Calculate investment scaling factor + // Base investment of CHF 500,000 = 1.0x multiplier + // Higher investments get multiplicative benefits for instance acquisition + const baseInvestment = 500000; + const investmentScaleFactor = Math.sqrt(inputs.investmentAmount / baseInvestment); + + // Calculate churn reduction factor based on investment + // Higher investment = better customer success = lower churn + const churnReductionFactor = Math.max(0.7, 1 - (inputs.investmentAmount - baseInvestment) / 2000000 * 0.3); + + // Calculate adjusted churn rate with investment-based reduction + const adjustedChurnRate = scenario.churnRate * churnReductionFactor; + + const totalMonths = inputs.timeframe * 12; + const monthlyData = []; + let currentInstances = 0; + let cumulativeCSPRevenue = 0; + let cumulativeServalaRevenue = 0; + let breakEvenMonth = null; + let npvBreakEvenMonth = null; + let totalDiscountedCashFlow = -inputs.investmentAmount; + + // Calculate monthly discount rate + const monthlyDiscountRate = Math.pow(1 + inputs.discountRate, 1 / 12) - 1; + + // Track phase progression + let currentPhase = 0; + let monthsInCurrentPhase = 0; + + for (let month = 1; month <= totalMonths; month++) { + // Determine current phase + if (monthsInCurrentPhase >= scenario.phases[currentPhase].months && currentPhase < scenario.phases.length - 1) { + currentPhase++; + monthsInCurrentPhase = 0; + } + + // Calculate new instances for this month with investment scaling + const baseNewInstances = scenario.phases[currentPhase].newInstancesPerMonth; + const newInstances = Math.floor(baseNewInstances * investmentScaleFactor); + + // Calculate churn using the pre-calculated adjusted churn rate + const churnedInstances = Math.floor(currentInstances * adjustedChurnRate); + + // Update total instances + currentInstances = currentInstances + newInstances - churnedInstances; + + // Calculate revenue + const monthlyRevenue = currentInstances * inputs.revenuePerInstance; + + // Determine revenue split based on grace period + let cspRevenue, servalaRevenue; + if (month <= inputs.gracePeriod) { + cspRevenue = monthlyRevenue; + servalaRevenue = 0; + } else { + cspRevenue = monthlyRevenue * (1 - inputs.servalaShare); + servalaRevenue = monthlyRevenue * inputs.servalaShare; + } + + // Update cumulative revenue + cumulativeCSPRevenue += cspRevenue; + cumulativeServalaRevenue += servalaRevenue; + + // Check for break-even + if (breakEvenMonth === null && cumulativeCSPRevenue >= inputs.investmentAmount) { + breakEvenMonth = month; + } + + // Calculate NPV break-even + const discountFactor = Math.pow(1 + monthlyDiscountRate, month); + const discountedCashFlow = cspRevenue / discountFactor; + totalDiscountedCashFlow += discountedCashFlow; + + if (npvBreakEvenMonth === null && totalDiscountedCashFlow >= 0) { + npvBreakEvenMonth = month; + } + + monthlyData.push({ + month, + scenario: scenario.name, + newInstances, + churnedInstances, + totalInstances: currentInstances, + monthlyRevenue, + cspRevenue, + servalaRevenue, + cumulativeCSPRevenue, + cumulativeServalaRevenue, + discountedCashFlow, + totalDiscountedCashFlow: totalDiscountedCashFlow + inputs.investmentAmount, + investmentScaleFactor: investmentScaleFactor, + adjustedChurnRate: adjustedChurnRate + }); + + monthsInCurrentPhase++; + } + + // Calculate final metrics + const totalRevenue = cumulativeCSPRevenue + cumulativeServalaRevenue; + const roi = ((cumulativeCSPRevenue - inputs.investmentAmount) / inputs.investmentAmount) * 100; + const npv = totalDiscountedCashFlow; + + return { + scenario: scenario.name, + finalInstances: currentInstances, + totalRevenue, + cspRevenue: cumulativeCSPRevenue, + servalaRevenue: cumulativeServalaRevenue, + roi, + npv, + breakEvenMonth, + npvBreakEvenMonth, + monthlyData, + investmentScaleFactor: investmentScaleFactor, + adjustedChurnRate: adjustedChurnRate * 100 + }; + } + + updateCalculations() { + const inputs = this.getInputValues(); + this.results = {}; + this.monthlyData = {}; + + // Show loading spinner + document.getElementById('loading-spinner').style.display = 'block'; + document.getElementById('summary-metrics').style.display = 'none'; + + // Calculate results for each enabled scenario + Object.keys(this.scenarios).forEach(scenarioKey => { + const result = this.calculateScenario(scenarioKey, inputs); + if (result) { + this.results[scenarioKey] = result; + this.monthlyData[scenarioKey] = result.monthlyData; + } + }); + + // Update UI + setTimeout(() => { + this.updateSummaryMetrics(); + this.updateCharts(); + this.updateComparisonTable(); + this.updateMonthlyBreakdown(); + + // Hide loading spinner + document.getElementById('loading-spinner').style.display = 'none'; + document.getElementById('summary-metrics').style.display = 'flex'; + }, 500); + } + + updateSummaryMetrics() { + const enabledResults = Object.values(this.results); + if (enabledResults.length === 0) { + document.getElementById('total-instances').textContent = '0'; + document.getElementById('total-revenue').textContent = 'CHF 0'; + document.getElementById('roi-percentage').textContent = '0%'; + document.getElementById('breakeven-time').textContent = 'N/A'; + return; + } + + // Calculate averages across enabled scenarios + const avgInstances = Math.round(enabledResults.reduce((sum, r) => sum + r.finalInstances, 0) / enabledResults.length); + const avgRevenue = enabledResults.reduce((sum, r) => sum + r.totalRevenue, 0) / enabledResults.length; + const avgROI = enabledResults.reduce((sum, r) => sum + r.roi, 0) / enabledResults.length; + const avgBreakeven = enabledResults.filter(r => r.breakEvenMonth).reduce((sum, r) => sum + r.breakEvenMonth, 0) / enabledResults.filter(r => r.breakEvenMonth).length; + + document.getElementById('total-instances').textContent = avgInstances.toLocaleString(); + document.getElementById('total-revenue').textContent = this.formatCurrency(avgRevenue); + document.getElementById('roi-percentage').textContent = this.formatPercentage(avgROI); + document.getElementById('breakeven-time').textContent = isNaN(avgBreakeven) ? 'N/A' : `${Math.round(avgBreakeven)} months`; + } + + initializeCharts() { + // Instance Growth Chart + const instanceCtx = document.getElementById('instanceGrowthChart').getContext('2d'); + this.charts.instanceGrowth = new Chart(instanceCtx, { + type: 'line', + data: { labels: [], datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' } + }, + scales: { + y: { + beginAtZero: true, + title: { display: true, text: 'Total Instances' } + }, + x: { + title: { display: true, text: 'Month' } + } + } + } + }); + + // Revenue Chart + const revenueCtx = document.getElementById('revenueChart').getContext('2d'); + this.charts.revenue = new Chart(revenueCtx, { + type: 'line', + data: { labels: [], datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' } + }, + scales: { + y: { + beginAtZero: true, + title: { display: true, text: 'Cumulative Revenue (CHF)' } + }, + x: { + title: { display: true, text: 'Month' } + } + } + } + }); + + // Cash Flow Chart + const cashFlowCtx = document.getElementById('cashFlowChart').getContext('2d'); + this.charts.cashFlow = new Chart(cashFlowCtx, { + type: 'bar', + data: { labels: [], datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' } + }, + scales: { + y: { + title: { display: true, text: 'Monthly Cash Flow (CHF)' } + }, + x: { + title: { display: true, text: 'Month' } + } + } + } + }); + } + + updateCharts() { + const scenarios = Object.keys(this.results); + if (scenarios.length === 0) return; + + const colors = { + conservative: '#28a745', + moderate: '#ffc107', + aggressive: '#dc3545' + }; + + // Get month labels + const maxMonths = Math.max(...scenarios.map(s => this.monthlyData[s].length)); + const monthLabels = Array.from({ length: maxMonths }, (_, i) => `M${i + 1}`); + + // Update Instance Growth Chart + this.charts.instanceGrowth.data.labels = monthLabels; + this.charts.instanceGrowth.data.datasets = scenarios.map(scenario => ({ + label: this.scenarios[scenario].name, + data: this.monthlyData[scenario].map(d => d.totalInstances), + borderColor: colors[scenario], + backgroundColor: colors[scenario] + '20', + tension: 0.4 + })); + this.charts.instanceGrowth.update(); + + // Update Revenue Chart + this.charts.revenue.data.labels = monthLabels; + this.charts.revenue.data.datasets = scenarios.map(scenario => ({ + label: this.scenarios[scenario].name + ' (CSP)', + data: this.monthlyData[scenario].map(d => d.cumulativeCSPRevenue), + borderColor: colors[scenario], + backgroundColor: colors[scenario] + '20', + tension: 0.4 + })); + this.charts.revenue.update(); + + // Update Cash Flow Chart (show average across scenarios) + const avgCashFlow = monthLabels.map((_, monthIndex) => { + const monthData = scenarios.map(scenario => + this.monthlyData[scenario][monthIndex]?.cspRevenue || 0 + ); + return monthData.reduce((sum, val) => sum + val, 0) / monthData.length; + }); + + this.charts.cashFlow.data.labels = monthLabels; + this.charts.cashFlow.data.datasets = [{ + label: 'Average Monthly CSP Revenue', + data: avgCashFlow, + backgroundColor: '#007bff' + }]; + this.charts.cashFlow.update(); + } + + updateComparisonTable() { + const tbody = document.getElementById('comparison-tbody'); + tbody.innerHTML = ''; + + Object.values(this.results).forEach(result => { + const row = tbody.insertRow(); + row.innerHTML = ` + ${result.scenario} + ${result.finalInstances.toLocaleString()} + ${this.formatCurrencyDetailed(result.totalRevenue)} + ${this.formatCurrencyDetailed(result.cspRevenue)} + ${this.formatCurrencyDetailed(result.servalaRevenue)} + ${this.formatPercentage(result.roi)} + ${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'} + ${result.npvBreakEvenMonth ? result.npvBreakEvenMonth + ' months' : 'N/A'} + `; + }); + } + + updateMonthlyBreakdown() { + const tbody = document.getElementById('monthly-tbody'); + tbody.innerHTML = ''; + + // Combine all monthly data and sort by month and scenario + const allData = []; + Object.keys(this.monthlyData).forEach(scenario => { + this.monthlyData[scenario].forEach(monthData => { + allData.push(monthData); + }); + }); + + allData.sort((a, b) => a.month - b.month || a.scenario.localeCompare(b.scenario)); + + allData.forEach(data => { + const row = tbody.insertRow(); + row.innerHTML = ` + ${data.month} + ${data.scenario} + ${data.newInstances} + ${data.churnedInstances} + ${data.totalInstances.toLocaleString()} + ${this.formatCurrencyDetailed(data.monthlyRevenue)} + ${this.formatCurrencyDetailed(data.cspRevenue)} + ${this.formatCurrencyDetailed(data.servalaRevenue)} + ${this.formatCurrencyDetailed(data.cumulativeCSPRevenue)} + `; + }); + } + + formatCurrency(amount) { + // Use compact notation for large numbers in metric cards + if (amount >= 1000000) { + return new Intl.NumberFormat('de-CH', { + style: 'currency', + currency: 'CHF', + notation: 'compact', + minimumFractionDigits: 0, + maximumFractionDigits: 1 + }).format(amount); + } else { + return new Intl.NumberFormat('de-CH', { + style: 'currency', + currency: 'CHF', + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(amount); + } + } + + formatCurrencyDetailed(amount) { + // Use full formatting for detailed views (tables, exports) + return new Intl.NumberFormat('de-CH', { + style: 'currency', + currency: 'CHF', + minimumFractionDigits: 0, + maximumFractionDigits: 0 + }).format(amount); + } + + formatPercentage(value) { + return new Intl.NumberFormat('de-CH', { + style: 'percent', + minimumFractionDigits: 1, + maximumFractionDigits: 1 + }).format(value / 100); + } +} + +// Initialize calculator +let calculator; + +document.addEventListener('DOMContentLoaded', function () { + // Initialize calculator + calculator = new ROICalculator(); + + // Initialize tooltips - check if Bootstrap is available + initializeTooltips(); + + // Check if export libraries are loaded and enable/disable buttons accordingly + checkExportLibraries(); +}); + +// Initialize tooltips with Bootstrap (loaded directly via CDN) +function 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); + initializeNativeTooltips(); + } + } else { + // Retry after a short delay for deferred scripts + setTimeout(initializeTooltips, 100); + } +} + +function initializeNativeTooltips() { + 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); + } + } + }); +} + +// Check if export libraries are loaded +function checkExportLibraries() { + 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); + } +} + +// Input update functions +function formatNumberWithCommas(num) { + return parseInt(num).toLocaleString('en-US'); +} + +function parseFormattedNumber(str) { + return parseFloat(str.replace(/,/g, '')) || 0; +} + +function handleInvestmentAmountInput(input) { + // Remove non-numeric characters except commas + let value = input.value.replace(/[^\d,]/g, ''); + + // Parse the numeric value + let numericValue = parseFormattedNumber(value); + + // Enforce min/max limits + if (numericValue < 100000) numericValue = 100000; + if (numericValue > 2000000) numericValue = 2000000; + + // Update the data attribute with the raw numeric value + input.setAttribute('data-value', numericValue.toString()); + + // Format and display the value with commas + input.value = formatNumberWithCommas(numericValue); + + // Update the slider + document.getElementById('investment-slider').value = numericValue; + + // Trigger calculations + updateCalculations(); +} + +function updateInvestmentAmount(value) { + const input = document.getElementById('investment-amount'); + input.setAttribute('data-value', value); + input.value = formatNumberWithCommas(value); + updateCalculations(); +} + +function updateDiscountRate(value) { + document.getElementById('discount-rate').value = value; + updateCalculations(); +} + +function updateRevenuePerInstance(value) { + document.getElementById('revenue-per-instance').value = value; + updateCalculations(); +} + +function updateServalaShare(value) { + document.getElementById('servala-share').value = value; + updateCalculations(); +} + +function updateGracePeriod(value) { + document.getElementById('grace-period').value = value; + updateCalculations(); +} + +function updateCalculations() { + if (calculator) { + calculator.updateCalculations(); + } +} + +// Advanced parameter functions +function updateScenarioChurn(scenarioKey, churnRate) { + calculator.scenarios[scenarioKey].churnRate = parseFloat(churnRate) / 100; + updateCalculations(); +} + +function updateScenarioPhase(scenarioKey, phaseIndex, newInstancesPerMonth) { + calculator.scenarios[scenarioKey].phases[phaseIndex].newInstancesPerMonth = parseInt(newInstancesPerMonth); + updateCalculations(); +} + +function resetAdvancedParameters() { + if (confirm('Reset all advanced parameters to default values?')) { + // Reset Conservative + calculator.scenarios.conservative.churnRate = 0.02; + calculator.scenarios.conservative.phases = [ + { months: 6, newInstancesPerMonth: 50 }, + { months: 6, newInstancesPerMonth: 75 }, + { months: 12, newInstancesPerMonth: 100 }, + { months: 12, newInstancesPerMonth: 150 } + ]; + + // Reset Moderate + calculator.scenarios.moderate.churnRate = 0.03; + calculator.scenarios.moderate.phases = [ + { months: 6, newInstancesPerMonth: 100 }, + { months: 6, newInstancesPerMonth: 200 }, + { months: 12, newInstancesPerMonth: 300 }, + { months: 12, newInstancesPerMonth: 400 } + ]; + + // Reset Aggressive + calculator.scenarios.aggressive.churnRate = 0.05; + calculator.scenarios.aggressive.phases = [ + { months: 6, newInstancesPerMonth: 200 }, + { months: 6, newInstancesPerMonth: 400 }, + { months: 12, newInstancesPerMonth: 600 }, + { months: 12, newInstancesPerMonth: 800 } + ]; + + // Update UI inputs + document.getElementById('conservative-churn').value = '2.0'; + document.getElementById('conservative-phase-0').value = '50'; + document.getElementById('conservative-phase-1').value = '75'; + document.getElementById('conservative-phase-2').value = '100'; + document.getElementById('conservative-phase-3').value = '150'; + + document.getElementById('moderate-churn').value = '3.0'; + document.getElementById('moderate-phase-0').value = '100'; + document.getElementById('moderate-phase-1').value = '200'; + document.getElementById('moderate-phase-2').value = '300'; + document.getElementById('moderate-phase-3').value = '400'; + + document.getElementById('aggressive-churn').value = '5.0'; + document.getElementById('aggressive-phase-0').value = '200'; + document.getElementById('aggressive-phase-1').value = '400'; + document.getElementById('aggressive-phase-2').value = '600'; + document.getElementById('aggressive-phase-3').value = '800'; + + updateCalculations(); + } +} + +// Scenario management +function toggleScenario(scenarioKey) { + const enabled = document.getElementById(scenarioKey + '-enabled').checked; + calculator.scenarios[scenarioKey].enabled = enabled; + + const card = document.getElementById(scenarioKey + '-card'); + if (enabled) { + card.classList.add('active'); + card.classList.remove('disabled'); + } else { + card.classList.remove('active'); + card.classList.add('disabled'); + } + + updateCalculations(); +} + +// UI utility functions +function toggleCollapsible(elementId) { + const content = document.getElementById(elementId); + const header = content.previousElementSibling; + const chevron = header.querySelector('.bi-chevron-down, .bi-chevron-up'); + + if (content.classList.contains('show')) { + content.classList.remove('show'); + chevron.classList.remove('bi-chevron-up'); + chevron.classList.add('bi-chevron-down'); + } else { + content.classList.add('show'); + chevron.classList.remove('bi-chevron-down'); + chevron.classList.add('bi-chevron-up'); + } +} + +// Export functions +function exportToPDF() { + // Check if jsPDF is available + if (typeof window.jspdf === 'undefined') { + alert('PDF export library is loading. Please try again in a moment.'); + return; + } + + try { + const { jsPDF } = window.jspdf; + const doc = new jsPDF(); + + // Add header + doc.setFontSize(20); + doc.setTextColor(0, 123, 255); // Bootstrap primary blue + doc.text('CSP ROI Calculator Report', 20, 25); + + // Add generation date + doc.setFontSize(10); + doc.setTextColor(100, 100, 100); + const currentDate = new Date().toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + doc.text(`Generated on: ${currentDate}`, 20, 35); + + // Reset text color + doc.setTextColor(0, 0, 0); + + // Add input parameters section + doc.setFontSize(16); + doc.text('Investment Parameters', 20, 50); + + const inputs = calculator.getInputValues(); + let yPos = 60; + + doc.setFontSize(11); + const params = [ + ['Investment Amount:', calculator.formatCurrencyDetailed(inputs.investmentAmount)], + ['Investment Timeframe:', `${inputs.timeframe} years`], + ['Discount Rate:', `${(inputs.discountRate * 100).toFixed(1)}%`], + ['Revenue per Instance:', calculator.formatCurrencyDetailed(inputs.revenuePerInstance)], + ['Servala Revenue Share:', `${(inputs.servalaShare * 100).toFixed(0)}%`], + ['Grace Period:', `${inputs.gracePeriod} months`] + ]; + + params.forEach(([label, value]) => { + doc.text(label, 25, yPos); + doc.text(value, 80, yPos); + yPos += 8; + }); + + // Add scenario results section + yPos += 10; + doc.setFontSize(16); + doc.text('Scenario Results', 20, yPos); + yPos += 10; + + doc.setFontSize(11); + Object.values(calculator.results).forEach(result => { + if (yPos > 250) { + doc.addPage(); + yPos = 20; + } + + // Scenario header + doc.setFontSize(14); + doc.setTextColor(0, 123, 255); + doc.text(`${result.scenario} Scenario`, 25, yPos); + yPos += 10; + + doc.setFontSize(11); + doc.setTextColor(0, 0, 0); + + const resultData = [ + ['Final Instances:', result.finalInstances.toLocaleString()], + ['Total Revenue:', calculator.formatCurrencyDetailed(result.totalRevenue)], + ['CSP Revenue:', calculator.formatCurrencyDetailed(result.cspRevenue)], + ['Servala Revenue:', calculator.formatCurrencyDetailed(result.servalaRevenue)], + ['ROI:', calculator.formatPercentage(result.roi)], + ['Break-even:', result.breakEvenMonth ? `${result.breakEvenMonth} months` : 'Not achieved'], + ['NPV Break-even:', result.npvBreakEvenMonth ? `${result.npvBreakEvenMonth} months` : 'Not achieved'] + ]; + + resultData.forEach(([label, value]) => { + doc.text(label, 30, yPos); + doc.text(value, 90, yPos); + yPos += 7; + }); + + yPos += 8; + }); + + // Add summary section + if (yPos > 220) { + doc.addPage(); + yPos = 20; + } + + yPos += 10; + doc.setFontSize(16); + doc.text('Executive Summary', 20, yPos); + yPos += 10; + + doc.setFontSize(11); + const enabledResults = Object.values(calculator.results); + if (enabledResults.length > 0) { + const avgROI = enabledResults.reduce((sum, r) => sum + r.roi, 0) / enabledResults.length; + const avgBreakeven = enabledResults.filter(r => r.breakEvenMonth).reduce((sum, r) => sum + r.breakEvenMonth, 0) / enabledResults.filter(r => r.breakEvenMonth).length; + + doc.text(`This analysis evaluates ${enabledResults.length} growth scenario(s) over a ${inputs.timeframe}-year period.`, 25, yPos); + yPos += 8; + doc.text(`Average projected ROI: ${calculator.formatPercentage(avgROI)}`, 25, yPos); + yPos += 8; + if (!isNaN(avgBreakeven)) { + doc.text(`Average break-even timeline: ${Math.round(avgBreakeven)} months`, 25, yPos); + yPos += 8; + } + + yPos += 5; + doc.text('Key assumptions:', 25, yPos); + yPos += 8; + doc.text('• Growth rates based on market analysis and industry benchmarks', 30, yPos); + yPos += 6; + doc.text('• Churn rates reflect typical SaaS industry standards', 30, yPos); + yPos += 6; + doc.text('• Revenue calculations include grace period provisions', 30, yPos); + yPos += 6; + doc.text('• NPV calculations use specified discount rate', 30, yPos); + } + + // Add footer + const pageCount = doc.internal.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(8); + doc.setTextColor(150, 150, 150); + doc.text(`Page ${i} of ${pageCount}`, doc.internal.pageSize.getWidth() - 30, doc.internal.pageSize.getHeight() - 10); + doc.text('Generated by Servala CSP ROI Calculator', 20, doc.internal.pageSize.getHeight() - 10); + } + + // Save the PDF + const filename = `servala-csp-roi-report-${new Date().toISOString().split('T')[0]}.pdf`; + doc.save(filename); + + } catch (error) { + console.error('PDF Export Error:', error); + alert('An error occurred while generating the PDF. Please try again or export as CSV instead.'); + } +} + +function exportToCSV() { + try { + // Create comprehensive CSV with summary and detailed data + let csvContent = 'CSP ROI Calculator Export\n'; + csvContent += `Generated on: ${new Date().toLocaleDateString()}\n\n`; + + // Add input parameters + csvContent += 'INPUT PARAMETERS\n'; + const inputs = calculator.getInputValues(); + csvContent += `Investment Amount,${inputs.investmentAmount}\n`; + csvContent += `Timeframe (years),${inputs.timeframe}\n`; + csvContent += `Discount Rate (%),${(inputs.discountRate * 100).toFixed(1)}\n`; + csvContent += `Revenue per Instance,${inputs.revenuePerInstance}\n`; + csvContent += `Servala Share (%),${(inputs.servalaShare * 100).toFixed(0)}\n`; + csvContent += `Grace Period (months),${inputs.gracePeriod}\n\n`; + + // Add scenario summary + csvContent += 'SCENARIO SUMMARY\n'; + csvContent += 'Scenario,Final Instances,Total Revenue,CSP Revenue,Servala Revenue,ROI (%),Break-even (months),NPV Break-even (months)\n'; + + Object.values(calculator.results).forEach(result => { + csvContent += `${result.scenario},${result.finalInstances},${result.totalRevenue.toFixed(2)},${result.cspRevenue.toFixed(2)},${result.servalaRevenue.toFixed(2)},${result.roi.toFixed(2)},${result.breakEvenMonth || 'N/A'},${result.npvBreakEvenMonth || 'N/A'}\n`; + }); + + csvContent += '\n'; + + // Add detailed monthly data + csvContent += 'MONTHLY BREAKDOWN\n'; + csvContent += 'Month,Scenario,New Instances,Churned Instances,Total Instances,Monthly Revenue,CSP Revenue,Servala Revenue,Cumulative CSP Revenue,Cumulative Servala Revenue\n'; + + // Combine all monthly data + const allData = []; + Object.keys(calculator.monthlyData).forEach(scenario => { + calculator.monthlyData[scenario].forEach(monthData => { + allData.push(monthData); + }); + }); + + allData.sort((a, b) => a.month - b.month || a.scenario.localeCompare(b.scenario)); + + allData.forEach(data => { + csvContent += `${data.month},${data.scenario},${data.newInstances},${data.churnedInstances},${data.totalInstances},${data.monthlyRevenue.toFixed(2)},${data.cspRevenue.toFixed(2)},${data.servalaRevenue.toFixed(2)},${data.cumulativeCSPRevenue.toFixed(2)},${data.cumulativeServalaRevenue.toFixed(2)}\n`; + }); + + // Create and download file + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + + const filename = `servala-csp-roi-data-${new Date().toISOString().split('T')[0]}.csv`; + link.setAttribute('download', filename); + link.style.visibility = 'hidden'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up + URL.revokeObjectURL(url); + + } catch (error) { + console.error('CSV Export Error:', error); + alert('An error occurred while generating the CSV file. Please try again.'); + } +} + +// Reset function +function resetCalculator() { + if (confirm('Are you sure you want to reset all parameters to default values?')) { + // Reset input values + const investmentInput = document.getElementById('investment-amount'); + investmentInput.setAttribute('data-value', '500000'); + investmentInput.value = '500,000'; + document.getElementById('investment-slider').value = 500000; + document.getElementById('timeframe').value = 3; + document.getElementById('discount-rate').value = 10; + document.getElementById('discount-slider').value = 10; + document.getElementById('revenue-per-instance').value = 50; + document.getElementById('revenue-slider').value = 50; + document.getElementById('servala-share').value = 25; + document.getElementById('share-slider').value = 25; + document.getElementById('grace-period').value = 6; + document.getElementById('grace-slider').value = 6; + + // Reset scenarios + ['conservative', 'moderate', 'aggressive'].forEach(scenario => { + document.getElementById(scenario + '-enabled').checked = true; + calculator.scenarios[scenario].enabled = true; + document.getElementById(scenario + '-card').classList.add('active'); + document.getElementById(scenario + '-card').classList.remove('disabled'); + }); + + // Reset advanced parameters + resetAdvancedParameters(); + + // Recalculate (this will be called by resetAdvancedParameters, but we ensure it happens) + updateCalculations(); + } +} + +// Logout function +function logout() { + if (confirm('Are you sure you want to logout?')) { + // Create a form to submit logout request + const form = document.createElement('form'); + form.method = 'POST'; + form.action = '{% url "services:csp_roi_calculator" %}'; + + // Add CSRF token + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = 'csrfmiddlewaretoken'; + csrfInput.value = '{{ csrf_token }}'; + 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(); + } +} diff --git a/hub/services/templates/calculator/csp_roi_calculator.html b/hub/services/templates/calculator/csp_roi_calculator.html index 1f7667b..c06d238 100644 --- a/hub/services/templates/calculator/csp_roi_calculator.html +++ b/hub/services/templates/calculator/csp_roi_calculator.html @@ -844,963 +844,5 @@ {% block extra_js %} - + {% endblock %}