1566 lines
65 KiB
HTML
1566 lines
65 KiB
HTML
{% 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;
|
|
}
|
|
|
|
.input-group-custom label {
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
display: block;
|
|
}
|
|
|
|
.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 {
|
|
font-size: 1.6rem;
|
|
font-weight: bold;
|
|
color: #007bff;
|
|
line-height: 1.2;
|
|
word-break: break-word;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.metric-label {
|
|
color: #6c757d;
|
|
font-size: 0.9rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.chart-container {
|
|
height: 300px;
|
|
}
|
|
|
|
.metric-value {
|
|
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;
|
|
}
|
|
}
|
|
</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">
|
|
<!-- 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)"
|
|
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 (%)</label>
|
|
<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%</small>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
</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>
|
|
<div class="metric-label">Total Instances</div>
|
|
</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>
|
|
<div class="metric-label">Total Revenue</div>
|
|
</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>
|
|
<div class="metric-label">Average ROI</div>
|
|
</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>
|
|
<div class="metric-label">Avg Break-even</div>
|
|
</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="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
<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')),
|
|
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>
|
|
<td>${this.formatCurrencyDetailed(result.totalRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(result.cspRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(result.servalaRevenue)}</td>
|
|
<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>
|
|
<td>${this.formatCurrencyDetailed(data.monthlyRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(data.cspRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(data.servalaRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(data.cumulativeCSPRevenue)}</td>
|
|
`;
|
|
});
|
|
}
|
|
|
|
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();
|
|
|
|
// Check if export libraries are loaded and enable/disable buttons accordingly
|
|
checkExportLibraries();
|
|
});
|
|
|
|
// 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();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|