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 %}