add core revenue to model and allow to tweak params

This commit is contained in:
Tobias Brunner 2025-07-23 14:29:54 +02:00
parent a07788cb74
commit 491dbacda4
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
7 changed files with 476 additions and 118 deletions

View file

@ -387,6 +387,116 @@
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Enhanced main configuration styling */
.main-config-section {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.main-config-section .form-label {
color: #495057;
font-weight: 600;
}
.main-config-section .input-group-text {
background-color: #e9ecef;
border-color: #ced4da;
font-weight: 500;
}
/* Advanced controls styling */
.advanced-controls-section {
background: #f8f9fa;
border-top: 3px solid #007bff;
}
.advanced-controls-section .card {
transition: all 0.2s ease;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.advanced-controls-section .card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.advanced-controls-section .card-header {
font-weight: 600;
border-bottom: 1px solid rgba(255,255,255,0.2);
}
/* Slider styling improvements */
.form-range {
height: 6px;
background: #e9ecef;
border-radius: 3px;
}
.form-range::-webkit-slider-thumb {
width: 18px;
height: 18px;
background: #007bff;
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
cursor: pointer;
}
.form-range::-moz-range-thumb {
width: 18px;
height: 18px;
background: #007bff;
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
cursor: pointer;
}
/* Real-time results cards */
.results-card {
transition: all 0.2s ease;
border: 2px solid transparent;
}
.results-card:hover {
border-color: #007bff;
transform: translateY(-1px);
}
/* Growth scenario checkboxes */
.form-check-input:checked {
background-color: #007bff;
border-color: #007bff;
}
.form-check-label {
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
}
.form-check-label:hover {
color: #007bff;
}
/* Responsive improvements */
@media (max-width: 768px) {
.main-config-section {
margin: 0 -15px;
border-radius: 0;
}
.advanced-controls-section .card {
margin-bottom: 1rem;
}
.advanced-controls-section .card-body {
padding: 1rem;
}
}
/* Model comparison box */
.model-comparison-box {
border: 1px solid #dee2e6;

View file

@ -78,12 +78,17 @@ class ROICalculator {
const graceElement = document.getElementById('grace-period');
const gracePeriod = graceElement ? parseInt(graceElement.value) || 6 : 6;
// Get core service revenue with validation
const coreRevenueElement = document.getElementById('core-service-revenue');
const coreServiceRevenue = coreRevenueElement ? parseFloat(coreRevenueElement.value) || 0 : 0;
return {
investmentAmount,
timeframe,
investmentModel,
loanInterestRate,
revenuePerInstance,
coreServiceRevenue,
servalaShare,
gracePeriod
};
@ -96,6 +101,7 @@ class ROICalculator {
investmentModel: 'direct',
loanInterestRate: 0.05,
revenuePerInstance: 50,
coreServiceRevenue: 0,
servalaShare: 0.25,
gracePeriod: 6
};
@ -204,7 +210,10 @@ class ROICalculator {
monthlyRevenue = monthlyLoanPayment;
} else {
// Direct investment model: Revenue based on instances with performance incentives
monthlyRevenue = currentInstances * inputs.revenuePerInstance;
// Service revenue (shared with Servala) + Core service revenue (100% to CSP)
const serviceRevenue = currentInstances * inputs.revenuePerInstance;
const coreRevenue = currentInstances * inputs.coreServiceRevenue;
monthlyRevenue = serviceRevenue;
// Calculate performance bonus if CSP exceeds baseline expectations
if (baselineInstances > 0 && month > 6) { // Start performance tracking after 6 months
@ -217,11 +226,13 @@ class ROICalculator {
// Determine revenue split based on dynamic grace period
if (month <= effectiveGracePeriod) {
cspRevenue = monthlyRevenue;
// During grace period: CSP keeps all service revenue + core revenue
cspRevenue = serviceRevenue + coreRevenue;
servalaRevenue = 0;
} else {
cspRevenue = monthlyRevenue * (1 - adjustedServalaShare);
servalaRevenue = monthlyRevenue * adjustedServalaShare;
// After grace period: CSP keeps share of service revenue + all core revenue
cspRevenue = (serviceRevenue * (1 - adjustedServalaShare)) + coreRevenue;
servalaRevenue = serviceRevenue * adjustedServalaShare;
}
}
@ -244,6 +255,18 @@ class ROICalculator {
breakEvenMonth = month;
}
// Calculate revenue components for data tracking
let serviceRevenueForData, coreRevenueForData, totalRevenueForData;
if (inputs.investmentModel === 'loan') {
serviceRevenueForData = monthlyLoanPayment;
coreRevenueForData = 0;
totalRevenueForData = monthlyLoanPayment;
} else {
serviceRevenueForData = currentInstances * inputs.revenuePerInstance;
coreRevenueForData = currentInstances * inputs.coreServiceRevenue;
totalRevenueForData = serviceRevenueForData + coreRevenueForData;
}
monthlyData.push({
month,
scenario: scenario.name,
@ -251,7 +274,10 @@ class ROICalculator {
churnedInstances,
totalInstances: currentInstances,
baselineInstances,
monthlyRevenue,
serviceRevenue: serviceRevenueForData,
coreRevenue: coreRevenueForData,
totalRevenue: totalRevenueForData,
monthlyRevenue: serviceRevenueForData, // Keep for backward compatibility
cspRevenue,
servalaRevenue,
cumulativeCSPRevenue,

View file

@ -230,7 +230,9 @@ class ExportManager {
const params = [
['Investment Amount', this.formatCHF(inputs.investmentAmount)],
['Investment Timeframe', `${inputs.timeframe} years`],
['Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance)} / month`],
['Service Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance)} / month`],
['Core Service Revenue per Instance', `${this.formatCHF(inputs.coreServiceRevenue)} / month`],
['Total Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance + inputs.coreServiceRevenue)} / month`],
['Loan Interest Rate', `${(inputs.loanInterestRate * 100).toFixed(1)}%`],
['Direct Investment Share', `${(inputs.servalaShare * 100).toFixed(0)}% to Servala`],
['Grace Period', `${inputs.gracePeriod} months`]
@ -526,7 +528,9 @@ class ExportManager {
if (inputs.investmentModel === 'loan') {
csvContent += `Loan Interest Rate (%),${(inputs.loanInterestRate * 100).toFixed(1)}\n`;
}
csvContent += `Revenue per Instance,${inputs.revenuePerInstance}\n`;
csvContent += `Service Revenue per Instance,${inputs.revenuePerInstance}\n`;
csvContent += `Core Service Revenue per Instance,${inputs.coreServiceRevenue}\n`;
csvContent += `Total Revenue per Instance,${inputs.revenuePerInstance + inputs.coreServiceRevenue}\n`;
if (inputs.investmentModel === 'direct') {
csvContent += `Servala Share (%),${(inputs.servalaShare * 100).toFixed(0)}\n`;
csvContent += `Grace Period (months),${inputs.gracePeriod}\n`;

View file

@ -198,6 +198,18 @@ class ROICalculatorApp {
}
}
updateCoreServiceRevenue(value) {
try {
const element = document.getElementById('core-service-revenue');
if (element) {
element.value = value;
this.updateCalculations();
}
} catch (error) {
console.error('Error updating core service revenue:', error);
}
}
updateScenarioChurn(scenarioKey, churnRate) {
try {
if (this.calculator && this.calculator.scenarios[scenarioKey]) {

View file

@ -183,7 +183,9 @@ class UIManager {
<td><span style="color: ${scenarioColor}" class="fw-bold">${data.scenario}</span></td>
<td>${modelBadge}</td>
<td class="text-end">${data.totalInstances ? data.totalInstances.toLocaleString() : '0'}</td>
<td class="text-end">${this.formatCurrencyDetailed(data.monthlyRevenue || 0)}</td>
<td class="text-end">${this.formatCurrencyDetailed(data.serviceRevenue || data.monthlyRevenue || 0)}</td>
<td class="text-end">${this.formatCurrencyDetailed(data.coreRevenue || 0)}</td>
<td class="text-end fw-bold">${this.formatCurrencyDetailed(data.totalRevenue || data.monthlyRevenue || 0)}</td>
<td class="text-end fw-bold">${this.formatCurrencyDetailed(data.cspRevenue || 0)}</td>
<td class="text-end text-muted">${this.formatCurrencyDetailed(data.servalaRevenue || 0)}</td>
<td class="text-end fw-bold ${netPositionClass}">${this.formatCurrencyDetailed(data.netPosition || 0)}</td>