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(); } }