website/hub/services/templates/calculator/csp_roi_calculator.html

1758 lines
75 KiB
HTML
Raw Normal View History

2025-07-16 15:12:25 +02:00
{% extends 'base.html' %}
{% load static %}
{% block title %}CSP ROI Calculator{% endblock %}
{% block extra_css %}
<link href="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.min.css" rel="stylesheet">
<style>
.calculator-section {
background: #f8f9fa;
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.input-group-custom {
margin-bottom: 1rem;
position: relative; /* Ensure proper stacking context */
2025-07-16 15:12:25 +02:00
}
.input-group-custom label {
font-weight: 600;
margin-bottom: 0.5rem;
display: block;
}
/* Ensure input groups don't interfere with tooltips */
.input-group {
position: relative;
z-index: 1;
}
2025-07-16 15:12:25 +02:00
.slider-container {
position: relative;
margin: 10px 0;
}
.slider {
width: 100%;
height: 8px;
border-radius: 5px;
background: #ddd;
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #007bff;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #007bff;
cursor: pointer;
border: none;
}
.scenario-card {
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.scenario-card.active {
border-color: #007bff;
background-color: #f8f9ff;
}
.scenario-card.disabled {
opacity: 0.6;
background-color: #f8f9fa;
}
.metric-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1rem;
}
.metric-value {
2025-07-16 15:15:34 +02:00
font-size: 1.6rem;
2025-07-16 15:12:25 +02:00
font-weight: bold;
color: #007bff;
2025-07-16 15:15:34 +02:00
line-height: 1.2;
word-break: break-word;
overflow-wrap: break-word;
2025-07-16 15:12:25 +02:00
}
.metric-label {
color: #6c757d;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
2025-07-16 15:15:34 +02:00
margin-top: 0.5rem;
2025-07-16 15:12:25 +02:00
}
.chart-container {
position: relative;
height: 400px;
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1.5rem;
}
.export-buttons {
position: sticky;
top: 20px;
background: white;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1rem;
}
.collapsible-section {
border: 1px solid #e9ecef;
border-radius: 8px;
margin-bottom: 1rem;
}
.collapsible-header {
background: #f8f9fa;
padding: 1rem;
cursor: pointer;
border-radius: 8px 8px 0 0;
transition: background-color 0.3s ease;
}
.collapsible-header:hover {
background: #e9ecef;
}
.collapsible-content {
padding: 1rem;
display: none;
}
.collapsible-content.show {
display: block;
}
.phase-settings {
background: #f8f9fa;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.currency-symbol {
font-weight: bold;
color: #28a745;
}
.loading-spinner {
display: none;
text-align: center;
padding: 2rem;
}
2025-07-16 15:39:06 +02:00
.help-content {
background: #f8f9fa;
border-radius: 6px;
padding: 1rem;
}
.help-content h6 {
margin-bottom: 0.5rem;
font-weight: 600;
}
.help-content p {
margin-bottom: 0.75rem;
line-height: 1.4;
}
.help-content p:last-child {
margin-bottom: 0;
}
/* Bootstrap tooltip styling improvements */
.tooltip {
font-size: 0.875rem;
font-family: inherit;
z-index: 9999 !important; /* Ensure tooltips appear above all other elements */
pointer-events: none; /* Prevent tooltip from interfering with mouse events */
}
.tooltip .tooltip-inner {
max-width: 250px;
padding: 0.5rem 0.75rem;
color: #fff;
background-color: #212529;
border-radius: 6px;
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.15);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.tooltip .tooltip-arrow {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
}
.tooltip.bs-tooltip-top .tooltip-arrow::before {
border-top-color: #212529;
}
.tooltip.bs-tooltip-bottom .tooltip-arrow::before {
border-bottom-color: #212529;
}
.tooltip.bs-tooltip-start .tooltip-arrow::before {
border-left-color: #212529;
}
.tooltip.bs-tooltip-end .tooltip-arrow::before {
border-right-color: #212529;
}
/* Enhanced cursor for tooltip elements */
2025-07-16 15:59:51 +02:00
[data-bs-toggle="tooltip"] {
cursor: help;
2025-07-16 15:59:51 +02:00
position: relative;
}
[data-bs-toggle="tooltip"]:hover {
opacity: 0.8;
}
2025-07-16 15:12:25 +02:00
@media (max-width: 768px) {
.chart-container {
height: 300px;
}
.metric-value {
2025-07-16 15:15:34 +02:00
font-size: 1.4rem;
}
.metric-card {
padding: 1rem;
}
}
@media (max-width: 576px) {
.metric-value {
font-size: 1.2rem;
}
.metric-card {
padding: 0.75rem;
margin-bottom: 0.75rem;
2025-07-16 15:12:25 +02:00
}
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid my-4">
<div class="row">
<!-- Header -->
<div class="col-12 mb-4">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h2 mb-1">CSP ROI Calculator</h1>
<p class="text-muted">Calculate potential returns from investing in Servala platform</p>
</div>
<div>
<button type="button" class="btn btn-outline-secondary me-2" onclick="resetCalculator()">
<i class="bi bi-arrow-clockwise"></i> Reset
</button>
<button type="button" class="btn btn-danger" onclick="logout()">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Input Panel -->
<div class="col-lg-4">
2025-07-16 15:39:06 +02:00
<!-- Help Section -->
<div class="collapsible-section mb-3">
<div class="collapsible-header" onclick="toggleCollapsible('help-section')">
<h5 class="mb-0">
<i class="bi bi-question-circle"></i> How the Calculator Works
<i class="bi bi-chevron-down float-end"></i>
</h5>
</div>
<div class="collapsible-content" id="help-section">
<div class="help-content">
<h6 class="text-primary mb-2"><i class="bi bi-lightbulb"></i> Calculator Overview</h6>
<p class="small">This ROI calculator models your investment in the Servala platform by simulating cloud service provider (CSP) business growth over time. It calculates potential returns based on instance growth, churn rates, and revenue sharing with Servala.</p>
<h6 class="text-primary mb-2 mt-3"><i class="bi bi-gear"></i> Key Parameters</h6>
<div class="small">
<p><strong>Investment Amount:</strong> Your initial capital investment in the Servala platform and infrastructure.</p>
<p><strong>Monthly Revenue per Instance:</strong> The recurring revenue generated from each managed service instance (excl. compute).</p>
<p><strong>Servala Revenue Share:</strong> Percentage of revenue shared with Servala after the grace period. This is Servala's platform fee.</p>
<p><strong>Grace Period:</strong> Initial months where you keep 100% of revenue before sharing begins with Servala.</p>
<p><strong>Discount Rate:</strong> Used for NPV calculations - represents your required rate of return or cost of capital.</p>
</div>
<h6 class="text-primary mb-2 mt-3"><i class="bi bi-graph-up"></i> Growth Scenarios</h6>
<div class="small">
<p><strong>Conservative:</strong> Steady growth with low churn (2%), suitable for established markets.</p>
<p><strong>Moderate:</strong> Balanced growth with moderate churn (3%), typical for competitive markets.</p>
<p><strong>Aggressive:</strong> Rapid expansion with higher churn (5%), for high-growth strategies.</p>
<p>Each scenario has 4 growth phases with customizable instance acquisition rates in Advanced Parameters.</p>
</div>
<h6 class="text-primary mb-2 mt-3"><i class="bi bi-calculator"></i> What is NPV?</h6>
<p class="small"><strong>Net Present Value (NPV)</strong> represents the current worth of future cash flows, discounted back to today's value using your discount rate. A positive NPV indicates the investment is profitable. NPV accounts for the time value of money - money received today is worth more than the same amount received in the future.</p>
<h6 class="text-primary mb-2 mt-3"><i class="bi bi-bullseye"></i> Understanding Results</h6>
<div class="small">
<p><strong>ROI:</strong> Return on Investment as a percentage of your initial investment.</p>
<p><strong>Break-even:</strong> Month when cumulative revenue equals your initial investment.</p>
<p><strong>NPV Break-even:</strong> Month when discounted cash flows become positive (more accurate timing).</p>
<p><strong>Churn:</strong> Monthly percentage of instances that stop generating revenue (customer loss).</p>
</div>
</div>
</div>
</div>
2025-07-16 15:12:25 +02:00
<!-- Export Buttons -->
<div class="export-buttons">
<h5><i class="bi bi-download"></i> Export Results</h5>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="exportToPDF()">
<i class="bi bi-file-pdf"></i> Export PDF Report
</button>
<button type="button" class="btn btn-outline-success btn-sm" onclick="exportToCSV()">
<i class="bi bi-file-csv"></i> Export CSV Data
</button>
</div>
</div>
<!-- Investment Settings -->
<div class="calculator-section">
<h4><i class="bi bi-cash-coin"></i> Investment Settings</h4>
<div class="input-group-custom">
<label for="investment-amount">Investment Amount</label>
<div class="input-group">
<span class="input-group-text currency-symbol">CHF</span>
<input type="text" class="form-control" id="investment-amount"
data-value="500000" value="500,000"
oninput="handleInvestmentAmountInput(this)"
2025-07-16 15:12:25 +02:00
onchange="updateCalculations()">
</div>
<div class="slider-container">
<input type="range" class="slider" id="investment-slider"
min="100000" max="2000000" step="10000" value="500000"
onchange="updateInvestmentAmount(this.value)">
</div>
<small class="text-muted">CHF 100,000 - CHF 2,000,000</small>
</div>
<div class="input-group-custom">
<label for="timeframe">Investment Timeframe (Years)</label>
<select class="form-select" id="timeframe" onchange="updateCalculations()">
<option value="1">1 Year</option>
<option value="2">2 Years</option>
<option value="3" selected>3 Years</option>
<option value="4">4 Years</option>
<option value="5">5 Years</option>
</select>
</div>
<div class="input-group-custom">
<label for="discount-rate">
Discount Rate for NPV (%)
<i class="bi bi-question-circle-fill text-muted ms-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="The discount rate represents your required rate of return or cost of capital. It's used to calculate the present value of future cash flows. A higher rate means you need higher returns to justify the investment. For example: 8-10% for conservative investors, 12-15% for moderate risk tolerance, 15-20% for high-risk/high-return expectations. This rate accounts for inflation, risk, and opportunity cost of your capital."
style="cursor: help; font-size: 0.8rem;"></i>
</label>
2025-07-16 15:12:25 +02:00
<input type="number" class="form-control" id="discount-rate"
min="5" max="20" step="0.5" value="10"
onchange="updateCalculations()">
<div class="slider-container">
<input type="range" class="slider" id="discount-slider"
min="5" max="20" step="0.5" value="10"
onchange="updateDiscountRate(this.value)">
</div>
<small class="text-muted">5% - 20% (your required annual return rate)</small>
2025-07-16 15:12:25 +02:00
</div>
</div>
<!-- Revenue Model -->
<div class="calculator-section">
<h4><i class="bi bi-graph-up"></i> Revenue Model</h4>
<div class="input-group-custom">
<label for="revenue-per-instance">Monthly Revenue per Instance</label>
<div class="input-group">
<span class="input-group-text currency-symbol">CHF</span>
<input type="number" class="form-control" id="revenue-per-instance"
min="20" max="200" step="5" value="50"
onchange="updateCalculations()">
</div>
<div class="slider-container">
<input type="range" class="slider" id="revenue-slider"
min="20" max="200" step="5" value="50"
onchange="updateRevenuePerInstance(this.value)">
</div>
<small class="text-muted">CHF 20 - CHF 200</small>
</div>
<div class="input-group-custom">
<label for="servala-share">Servala Revenue Share (%)</label>
<input type="number" class="form-control" id="servala-share"
min="10" max="40" step="1" value="25"
onchange="updateCalculations()">
<div class="slider-container">
<input type="range" class="slider" id="share-slider"
min="10" max="40" step="1" value="25"
onchange="updateServalaShare(this.value)">
</div>
<small class="text-muted">10% - 40%</small>
</div>
<div class="input-group-custom">
<label for="grace-period">Grace Period (Months)</label>
<input type="number" class="form-control" id="grace-period"
min="0" max="24" step="1" value="6"
onchange="updateCalculations()">
<div class="slider-container">
<input type="range" class="slider" id="grace-slider"
min="0" max="24" step="1" value="6"
onchange="updateGracePeriod(this.value)">
</div>
<small class="text-muted">0 - 24 months (100% revenue to CSP)</small>
</div>
</div>
<!-- Growth Scenarios -->
<div class="calculator-section">
<h4><i class="bi bi-speedometer2"></i> Growth Scenarios</h4>
<div class="scenario-card active" id="conservative-card">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="conservative-enabled" checked onchange="toggleScenario('conservative')">
<label class="form-check-label fw-bold" for="conservative-enabled">
Conservative Growth
</label>
</div>
<p class="small text-muted mb-2">Steady, predictable growth with minimal risk</p>
<small class="text-info">Churn: 2% | New instances: 50-150/month (customizable below)</small>
2025-07-16 15:12:25 +02:00
</div>
<div class="scenario-card active" id="moderate-card">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="moderate-enabled" checked onchange="toggleScenario('moderate')">
<label class="form-check-label fw-bold" for="moderate-enabled">
Moderate Growth
</label>
</div>
<p class="small text-muted mb-2">Balanced approach with moderate risk/reward</p>
<small class="text-info">Churn: 3% | New instances: 100-400/month (customizable below)</small>
2025-07-16 15:12:25 +02:00
</div>
<div class="scenario-card active" id="aggressive-card">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="aggressive-enabled" checked onchange="toggleScenario('aggressive')">
<label class="form-check-label fw-bold" for="aggressive-enabled">
Aggressive Growth
</label>
</div>
<p class="small text-muted mb-2">Rapid expansion with higher risk/reward</p>
<small class="text-info">Churn: 5% | New instances: 200-800/month (customizable below)</small>
2025-07-16 15:12:25 +02:00
</div>
<!-- Advanced Parameters -->
<div class="collapsible-section">
<div class="collapsible-header" onclick="toggleCollapsible('advanced-params')">
<h6 class="mb-0">
<i class="bi bi-gear"></i> Advanced Parameters
<i class="bi bi-chevron-down float-end"></i>
</h6>
</div>
<div class="collapsible-content" id="advanced-params">
<div id="scenario-customization">
<p class="text-muted small mb-3">Customize growth phases and churn rates for each scenario. Changes apply immediately to calculations.</p>
<!-- Conservative Scenario Customization -->
<div class="phase-settings" id="conservative-advanced" style="display: block;">
<h6 class="text-success mb-3"><i class="bi bi-sliders"></i> Conservative Scenario Parameters</h6>
<div class="row mb-2">
<div class="col-6">
<label class="form-label small">Monthly Churn Rate (%)</label>
<input type="number" class="form-control form-control-sm" id="conservative-churn"
min="0" max="10" step="0.1" value="2.0"
onchange="updateScenarioChurn('conservative', this.value)">
</div>
</div>
<div class="row mb-2">
<div class="col-6">
<label class="form-label small">Phase 1 (Mo 1-6)</label>
<input type="number" class="form-control form-control-sm" id="conservative-phase-0"
min="10" max="500" step="5" value="50"
onchange="updateScenarioPhase('conservative', 0, this.value)">
<small class="text-muted">instances/month</small>
</div>
<div class="col-6">
<label class="form-label small">Phase 2 (Mo 7-12)</label>
<input type="number" class="form-control form-control-sm" id="conservative-phase-1"
min="10" max="500" step="5" value="75"
onchange="updateScenarioPhase('conservative', 1, this.value)">
<small class="text-muted">instances/month</small>
</div>
</div>
<div class="row">
<div class="col-6">
<label class="form-label small">Phase 3 (Mo 13-24)</label>
<input type="number" class="form-control form-control-sm" id="conservative-phase-2"
min="10" max="500" step="5" value="100"
onchange="updateScenarioPhase('conservative', 2, this.value)">
<small class="text-muted">instances/month</small>
</div>
<div class="col-6">
<label class="form-label small">Phase 4 (Mo 25+)</label>
<input type="number" class="form-control form-control-sm" id="conservative-phase-3"
min="10" max="500" step="5" value="150"
onchange="updateScenarioPhase('conservative', 3, this.value)">
<small class="text-muted">instances/month</small>
</div>
</div>
</div>
<!-- Moderate Scenario Customization -->
<div class="phase-settings" id="moderate-advanced" style="display: block;">
<h6 class="text-warning mb-3"><i class="bi bi-sliders"></i> Moderate Scenario Parameters</h6>
<div class="row mb-2">
<div class="col-6">
<label class="form-label small">Monthly Churn Rate (%)</label>
<input type="number" class="form-control form-control-sm" id="moderate-churn"
min="0" max="10" step="0.1" value="3.0"
onchange="updateScenarioChurn('moderate', this.value)">
</div>
</div>
<div class="row mb-2">
<div class="col-6">
<label class="form-label small">Phase 1 (Mo 1-6)</label>
<input type="number" class="form-control form-control-sm" id="moderate-phase-0"
min="10" max="1000" step="10" value="100"
onchange="updateScenarioPhase('moderate', 0, this.value)">
<small class="text-muted">instances/month</small>
</div>
<div class="col-6">
<label class="form-label small">Phase 2 (Mo 7-12)</label>
<input type="number" class="form-control form-control-sm" id="moderate-phase-1"
min="10" max="1000" step="10" value="200"
onchange="updateScenarioPhase('moderate', 1, this.value)">
<small class="text-muted">instances/month</small>
</div>
</div>
<div class="row">
<div class="col-6">
<label class="form-label small">Phase 3 (Mo 13-24)</label>
<input type="number" class="form-control form-control-sm" id="moderate-phase-2"
min="10" max="1000" step="10" value="300"
onchange="updateScenarioPhase('moderate', 2, this.value)">
<small class="text-muted">instances/month</small>
</div>
<div class="col-6">
<label class="form-label small">Phase 4 (Mo 25+)</label>
<input type="number" class="form-control form-control-sm" id="moderate-phase-3"
min="10" max="1000" step="10" value="400"
onchange="updateScenarioPhase('moderate', 3, this.value)">
<small class="text-muted">instances/month</small>
</div>
</div>
</div>
<!-- Aggressive Scenario Customization -->
<div class="phase-settings" id="aggressive-advanced" style="display: block;">
<h6 class="text-danger mb-3"><i class="bi bi-sliders"></i> Aggressive Scenario Parameters</h6>
<div class="row mb-2">
<div class="col-6">
<label class="form-label small">Monthly Churn Rate (%)</label>
<input type="number" class="form-control form-control-sm" id="aggressive-churn"
min="0" max="15" step="0.1" value="5.0"
onchange="updateScenarioChurn('aggressive', this.value)">
</div>
</div>
<div class="row mb-2">
<div class="col-6">
<label class="form-label small">Phase 1 (Mo 1-6)</label>
<input type="number" class="form-control form-control-sm" id="aggressive-phase-0"
min="50" max="2000" step="25" value="200"
onchange="updateScenarioPhase('aggressive', 0, this.value)">
<small class="text-muted">instances/month</small>
</div>
<div class="col-6">
<label class="form-label small">Phase 2 (Mo 7-12)</label>
<input type="number" class="form-control form-control-sm" id="aggressive-phase-1"
min="50" max="2000" step="25" value="400"
onchange="updateScenarioPhase('aggressive', 1, this.value)">
<small class="text-muted">instances/month</small>
</div>
</div>
<div class="row">
<div class="col-6">
<label class="form-label small">Phase 3 (Mo 13-24)</label>
<input type="number" class="form-control form-control-sm" id="aggressive-phase-2"
min="50" max="2000" step="25" value="600"
onchange="updateScenarioPhase('aggressive', 2, this.value)">
<small class="text-muted">instances/month</small>
</div>
<div class="col-6">
<label class="form-label small">Phase 4 (Mo 25+)</label>
<input type="number" class="form-control form-control-sm" id="aggressive-phase-3"
min="50" max="2000" step="25" value="800"
onchange="updateScenarioPhase('aggressive', 3, this.value)">
<small class="text-muted">instances/month</small>
</div>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetAdvancedParameters()">
<i class="bi bi-arrow-clockwise"></i> Reset to Defaults
</button>
</div>
2025-07-16 15:12:25 +02:00
</div>
</div>
</div>
</div>
</div>
<!-- Results Display -->
<div class="col-lg-8">
<!-- Loading Spinner -->
<div class="loading-spinner" id="loading-spinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Calculating...</span>
</div>
<p class="mt-2">Calculating scenarios...</p>
</div>
<!-- Summary Metrics -->
<div class="row" id="summary-metrics">
<div class="col-md-3 col-sm-6">
<div class="metric-card text-center">
<div class="metric-value" id="total-instances">0</div>
2025-07-16 15:59:51 +02:00
<div class="metric-label">
Total Instances
<i class="bi bi-question-circle-fill text-muted ms-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Average final number of cloud instances across all enabled scenarios at the end of your investment timeframe. This represents the scale of your cloud infrastructure business."
style="cursor: help; font-size: 0.8rem;"></i>
</div>
2025-07-16 15:12:25 +02:00
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="metric-card text-center">
<div class="metric-value" id="total-revenue">CHF 0</div>
2025-07-16 15:59:51 +02:00
<div class="metric-label">
Total Revenue
<i class="bi bi-question-circle-fill text-muted ms-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Average total revenue generated across all enabled scenarios over your investment timeframe. This includes both CSP revenue (what you keep) and Servala revenue (platform fees)."
style="cursor: help; font-size: 0.8rem;"></i>
</div>
2025-07-16 15:12:25 +02:00
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="metric-card text-center">
<div class="metric-value" id="roi-percentage">0%</div>
2025-07-16 15:59:51 +02:00
<div class="metric-label">
Average ROI
<i class="bi bi-question-circle-fill text-muted ms-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Return on Investment: Average percentage return across all enabled scenarios. Calculated as (CSP Revenue - Initial Investment) / Initial Investment × 100%. Shows how much profit you make relative to your initial investment."
style="cursor: help; font-size: 0.8rem;"></i>
</div>
2025-07-16 15:12:25 +02:00
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="metric-card text-center">
<div class="metric-value" id="breakeven-time">N/A</div>
2025-07-16 15:59:51 +02:00
<div class="metric-label">
Avg Break-even
<i class="bi bi-question-circle-fill text-muted ms-1"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Average time in months across all scenarios when your cumulative CSP revenue equals your initial investment. This is when you've recovered your upfront costs and start making pure profit."
style="cursor: help; font-size: 0.8rem;"></i>
</div>
2025-07-16 15:12:25 +02:00
</div>
</div>
</div>
<!-- Charts -->
<div class="row">
<div class="col-12">
<div class="chart-container">
<h5><i class="bi bi-graph-up-arrow"></i> Instance Growth Over Time</h5>
<canvas id="instanceGrowthChart"></canvas>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="chart-container">
<h5><i class="bi bi-cash-stack"></i> Cumulative Revenue</h5>
<canvas id="revenueChart"></canvas>
</div>
</div>
<div class="col-md-6">
<div class="chart-container">
<h5><i class="bi bi-bar-chart"></i> Monthly Cash Flow</h5>
<canvas id="cashFlowChart"></canvas>
</div>
</div>
</div>
<!-- Scenario Comparison Table -->
<div class="row">
<div class="col-12">
<div class="chart-container">
<h5><i class="bi bi-table"></i> Scenario Comparison</h5>
<div class="table-responsive">
<table class="table table-striped" id="comparison-table">
<thead class="table-dark">
<tr>
<th>Scenario</th>
<th>Final Instances</th>
<th>Total Revenue</th>
<th>CSP Revenue</th>
<th>Servala Revenue</th>
<th>ROI</th>
<th>Break-even</th>
<th>NPV Break-even</th>
</tr>
</thead>
<tbody id="comparison-tbody">
<!-- Dynamic content -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Monthly Breakdown -->
<div class="row">
<div class="col-12">
<div class="collapsible-section">
<div class="collapsible-header" onclick="toggleCollapsible('monthly-breakdown')">
<h5 class="mb-0">
<i class="bi bi-calendar3"></i> Monthly Breakdown
<i class="bi bi-chevron-down float-end"></i>
</h5>
</div>
<div class="collapsible-content" id="monthly-breakdown">
<div class="chart-container">
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-sm table-striped" id="monthly-table">
<thead class="table-dark sticky-top">
<tr>
<th>Month</th>
<th>Scenario</th>
<th>New Instances</th>
<th>Churned</th>
<th>Total Instances</th>
<th>Monthly Revenue</th>
<th>CSP Revenue</th>
<th>Servala Revenue</th>
<th>Cumulative CSP</th>
</tr>
</thead>
<tbody id="monthly-tbody">
<!-- Dynamic content -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static "js/chart.js" %}"></script>
<script src="{% static "js/jspdf.umd.min.js" %}"></script>
2025-07-16 15:12:25 +02:00
<script>
// ROI Calculator JavaScript Implementation
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')),
2025-07-16 15:12:25 +02:00
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;
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
const newInstances = scenario.phases[currentPhase].newInstancesPerMonth;
// Calculate churn
const churnedInstances = Math.floor(currentInstances * scenario.churnRate);
// 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
});
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
};
}
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 = `
<td><strong>${result.scenario}</strong></td>
<td>${result.finalInstances.toLocaleString()}</td>
2025-07-16 15:15:34 +02:00
<td>${this.formatCurrencyDetailed(result.totalRevenue)}</td>
<td>${this.formatCurrencyDetailed(result.cspRevenue)}</td>
<td>${this.formatCurrencyDetailed(result.servalaRevenue)}</td>
2025-07-16 15:12:25 +02:00
<td class="${result.roi >= 0 ? 'text-success' : 'text-danger'}">${this.formatPercentage(result.roi)}</td>
<td>${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'}</td>
<td>${result.npvBreakEvenMonth ? result.npvBreakEvenMonth + ' months' : 'N/A'}</td>
`;
});
}
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 = `
<td>${data.month}</td>
<td><span class="badge bg-secondary">${data.scenario}</span></td>
<td>${data.newInstances}</td>
<td>${data.churnedInstances}</td>
<td>${data.totalInstances.toLocaleString()}</td>
2025-07-16 15:15:34 +02:00
<td>${this.formatCurrencyDetailed(data.monthlyRevenue)}</td>
<td>${this.formatCurrencyDetailed(data.cspRevenue)}</td>
<td>${this.formatCurrencyDetailed(data.servalaRevenue)}</td>
<td>${this.formatCurrencyDetailed(data.cumulativeCSPRevenue)}</td>
2025-07-16 15:12:25 +02:00
`;
});
}
formatCurrency(amount) {
2025-07-16 15:15:34 +02:00
// 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)
2025-07-16 15:12:25 +02:00
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();
2025-07-16 15:59:51 +02:00
// Initialize tooltips - check if Bootstrap is available
initializeTooltips();
2025-07-16 15:12:25 +02:00
// Check if export libraries are loaded and enable/disable buttons accordingly
checkExportLibraries();
});
// Initialize tooltips with Bootstrap (loaded directly via CDN)
2025-07-16 15:59:51 +02:00
function initializeTooltips() {
// Wait for Bootstrap to be available
2025-07-16 15:59:51 +02:00
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();
}
2025-07-16 15:59:51 +02:00
} 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');
2025-07-16 15:59:51 +02:00
if (tooltipContent) {
element.setAttribute('title', tooltipContent);
}
}
});
2025-07-16 15:59:51 +02:00
}
2025-07-16 15:12:25 +02:00
// 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 = '<i class="bi bi-file-pdf"></i> Loading PDF...';
}
// Retry after a short delay
setTimeout(() => {
if (typeof window.jspdf !== 'undefined' && pdfButton) {
pdfButton.disabled = false;
pdfButton.innerHTML = '<i class="bi bi-file-pdf"></i> 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();
}
2025-07-16 15:12:25 +02:00
function updateInvestmentAmount(value) {
const input = document.getElementById('investment-amount');
input.setAttribute('data-value', value);
input.value = formatNumberWithCommas(value);
2025-07-16 15:12:25 +02:00
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();
}
}
2025-07-16 15:12:25 +02:00
// 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 = [
2025-07-16 15:15:34 +02:00
['Investment Amount:', calculator.formatCurrencyDetailed(inputs.investmentAmount)],
2025-07-16 15:12:25 +02:00
['Investment Timeframe:', `${inputs.timeframe} years`],
['Discount Rate:', `${(inputs.discountRate * 100).toFixed(1)}%`],
2025-07-16 15:15:34 +02:00
['Revenue per Instance:', calculator.formatCurrencyDetailed(inputs.revenuePerInstance)],
2025-07-16 15:12:25 +02:00
['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()],
2025-07-16 15:15:34 +02:00
['Total Revenue:', calculator.formatCurrencyDetailed(result.totalRevenue)],
['CSP Revenue:', calculator.formatCurrencyDetailed(result.cspRevenue)],
['Servala Revenue:', calculator.formatCurrencyDetailed(result.servalaRevenue)],
2025-07-16 15:12:25 +02:00
['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';
2025-07-16 15:12:25 +02:00
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)
2025-07-16 15:12:25 +02:00
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();
}
}
</script>
{% endblock %}