rework investment model
This commit is contained in:
parent
22bea2c53d
commit
6f6c80480f
6 changed files with 633 additions and 141 deletions
|
|
@ -122,15 +122,32 @@ class ROICalculator {
|
|||
}
|
||||
}
|
||||
|
||||
// Calculate investment scaling factor (only for direct investment)
|
||||
// Base investment of CHF 500,000 = 1.0x multiplier
|
||||
// Higher investments get multiplicative benefits for instance acquisition
|
||||
// Enhanced investment scaling factor (only for direct investment)
|
||||
// Progressive scaling provides clear incentives for larger investments
|
||||
const baseInvestment = 500000;
|
||||
const investmentScaleFactor = inputs.investmentModel === 'loan' ? 1.0 : Math.sqrt(inputs.investmentAmount / baseInvestment);
|
||||
let investmentScaleFactor;
|
||||
let churnReductionFactor;
|
||||
|
||||
// Calculate churn reduction factor based on investment (only for direct investment)
|
||||
// Higher investment = better customer success = lower churn
|
||||
const churnReductionFactor = inputs.investmentModel === 'loan' ? 1.0 : Math.max(0.7, 1 - (inputs.investmentAmount - baseInvestment) / 2000000 * 0.3);
|
||||
if (inputs.investmentModel === 'loan') {
|
||||
investmentScaleFactor = 1.0;
|
||||
churnReductionFactor = 1.0;
|
||||
} else {
|
||||
// Progressive linear scaling with clear breakpoints
|
||||
if (inputs.investmentAmount <= baseInvestment) {
|
||||
investmentScaleFactor = inputs.investmentAmount / baseInvestment;
|
||||
} else if (inputs.investmentAmount <= 1000000) {
|
||||
// 500k to 1M: Scale from 1.0x to 1.5x
|
||||
investmentScaleFactor = 1.0 + (inputs.investmentAmount - baseInvestment) / 1000000;
|
||||
} else {
|
||||
// 1M to 2M: Scale from 1.5x to 2.0x
|
||||
investmentScaleFactor = 1.5 + (inputs.investmentAmount - 1000000) / 2000000;
|
||||
}
|
||||
|
||||
// Enhanced churn reduction with better scaling
|
||||
// Higher investment = better customer success = lower churn
|
||||
const churnReductionRatio = Math.min((inputs.investmentAmount - baseInvestment) / 1500000, 1.0);
|
||||
churnReductionFactor = Math.max(0.6, 1 - (churnReductionRatio * 0.4));
|
||||
}
|
||||
|
||||
// Calculate adjusted churn rate with investment-based reduction
|
||||
const adjustedChurnRate = scenario.churnRate * churnReductionFactor;
|
||||
|
|
@ -142,6 +159,15 @@ class ROICalculator {
|
|||
let cumulativeServalaRevenue = 0;
|
||||
let breakEvenMonth = null;
|
||||
|
||||
// Calculate dynamic grace period based on investment size
|
||||
const baseGracePeriod = inputs.gracePeriod;
|
||||
const gracePeriodBonus = Math.floor((inputs.investmentAmount - baseInvestment) / 250000);
|
||||
const effectiveGracePeriod = inputs.investmentModel === 'loan' ? 0 :
|
||||
Math.min(baseGracePeriod + gracePeriodBonus, Math.floor(totalMonths / 2));
|
||||
|
||||
// Track baseline performance for performance bonuses (direct investment only)
|
||||
let baselineInstances = 0; // Will track expected instances without performance scaling
|
||||
|
||||
// Track phase progression
|
||||
let currentPhase = 0;
|
||||
let monthsInCurrentPhase = 0;
|
||||
|
|
@ -157,6 +183,11 @@ class ROICalculator {
|
|||
const baseNewInstances = scenario.phases[currentPhase].newInstancesPerMonth;
|
||||
const newInstances = Math.floor(baseNewInstances * investmentScaleFactor);
|
||||
|
||||
// Track baseline instances (without investment scaling) for performance comparison
|
||||
const baselineNewInstances = baseNewInstances;
|
||||
const baselineChurnedInstances = Math.floor(baselineInstances * scenario.churnRate);
|
||||
baselineInstances = baselineInstances + baselineNewInstances - baselineChurnedInstances;
|
||||
|
||||
// Calculate churn using the pre-calculated adjusted churn rate
|
||||
const churnedInstances = Math.floor(currentInstances * adjustedChurnRate);
|
||||
|
||||
|
|
@ -164,24 +195,33 @@ class ROICalculator {
|
|||
currentInstances = currentInstances + newInstances - churnedInstances;
|
||||
|
||||
// Calculate revenue based on investment model
|
||||
let cspRevenue, servalaRevenue, monthlyRevenue;
|
||||
let cspRevenue, servalaRevenue, monthlyRevenue, performanceBonus = 0, adjustedServalaShare = inputs.servalaShare;
|
||||
|
||||
if (inputs.investmentModel === 'loan') {
|
||||
// Loan model: CSP receives fixed monthly loan payment
|
||||
// Loan model: CSP receives fixed monthly loan payment (predictable returns)
|
||||
cspRevenue = monthlyLoanPayment;
|
||||
servalaRevenue = 0;
|
||||
monthlyRevenue = monthlyLoanPayment;
|
||||
} else {
|
||||
// Direct investment model: Revenue based on instances
|
||||
// Direct investment model: Revenue based on instances with performance incentives
|
||||
monthlyRevenue = currentInstances * inputs.revenuePerInstance;
|
||||
|
||||
// Determine revenue split based on grace period
|
||||
if (month <= inputs.gracePeriod) {
|
||||
// Calculate performance bonus if CSP exceeds baseline expectations
|
||||
if (baselineInstances > 0 && month > 6) { // Start performance tracking after 6 months
|
||||
const performanceRatio = currentInstances / Math.max(baselineInstances, 1);
|
||||
if (performanceRatio > 1.1) { // 10% threshold for performance bonus
|
||||
performanceBonus = Math.max(0, Math.min(0.15, (performanceRatio - 1.1) * 0.3)); // Up to 15% bonus
|
||||
adjustedServalaShare = Math.max(0.10, inputs.servalaShare - performanceBonus);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine revenue split based on dynamic grace period
|
||||
if (month <= effectiveGracePeriod) {
|
||||
cspRevenue = monthlyRevenue;
|
||||
servalaRevenue = 0;
|
||||
} else {
|
||||
cspRevenue = monthlyRevenue * (1 - inputs.servalaShare);
|
||||
servalaRevenue = monthlyRevenue * inputs.servalaShare;
|
||||
cspRevenue = monthlyRevenue * (1 - adjustedServalaShare);
|
||||
servalaRevenue = monthlyRevenue * adjustedServalaShare;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,8 +229,18 @@ class ROICalculator {
|
|||
cumulativeCSPRevenue += cspRevenue;
|
||||
cumulativeServalaRevenue += servalaRevenue;
|
||||
|
||||
// Check for break-even
|
||||
if (breakEvenMonth === null && cumulativeCSPRevenue >= inputs.investmentAmount) {
|
||||
// Enhanced break-even calculation
|
||||
let netPosition; // CSP's net financial position
|
||||
if (inputs.investmentModel === 'loan') {
|
||||
// For loan model: net position is cumulative payments received minus loan principal outstanding
|
||||
const principalPaid = inputs.investmentAmount * (month / totalMonths); // Simplified principal tracking
|
||||
netPosition = cumulativeCSPRevenue - (inputs.investmentAmount - principalPaid);
|
||||
} else {
|
||||
// For direct investment: net position is cumulative revenue minus initial investment
|
||||
netPosition = cumulativeCSPRevenue - inputs.investmentAmount;
|
||||
}
|
||||
|
||||
if (breakEvenMonth === null && netPosition >= 0) {
|
||||
breakEvenMonth = month;
|
||||
}
|
||||
|
||||
|
|
@ -200,34 +250,55 @@ class ROICalculator {
|
|||
newInstances,
|
||||
churnedInstances,
|
||||
totalInstances: currentInstances,
|
||||
baselineInstances,
|
||||
monthlyRevenue,
|
||||
cspRevenue,
|
||||
servalaRevenue,
|
||||
cumulativeCSPRevenue,
|
||||
cumulativeServalaRevenue,
|
||||
netPosition,
|
||||
performanceBonus,
|
||||
adjustedServalaShare,
|
||||
effectiveGracePeriod,
|
||||
investmentScaleFactor: investmentScaleFactor,
|
||||
adjustedChurnRate: adjustedChurnRate
|
||||
adjustedChurnRate: adjustedChurnRate,
|
||||
roiPercent: inputs.investmentModel === 'loan' ?
|
||||
((cumulativeCSPRevenue - inputs.investmentAmount) / inputs.investmentAmount * 100) :
|
||||
(netPosition / inputs.investmentAmount * 100)
|
||||
});
|
||||
|
||||
monthsInCurrentPhase++;
|
||||
}
|
||||
|
||||
// Calculate final metrics
|
||||
// Calculate final metrics with enhanced business intelligence
|
||||
const totalRevenue = cumulativeCSPRevenue + cumulativeServalaRevenue;
|
||||
const roi = ((cumulativeCSPRevenue - inputs.investmentAmount) / inputs.investmentAmount) * 100;
|
||||
const finalNetPosition = inputs.investmentModel === 'loan' ?
|
||||
cumulativeCSPRevenue - inputs.investmentAmount :
|
||||
cumulativeCSPRevenue - inputs.investmentAmount;
|
||||
const roi = (finalNetPosition / inputs.investmentAmount) * 100;
|
||||
|
||||
// Calculate average performance bonus over the investment period
|
||||
const performanceBonusMonths = monthlyData.filter(m => m.performanceBonus > 0);
|
||||
const avgPerformanceBonus = performanceBonusMonths.length > 0 ?
|
||||
performanceBonusMonths.reduce((sum, m) => sum + m.performanceBonus, 0) / performanceBonusMonths.length : 0;
|
||||
|
||||
return {
|
||||
scenario: scenario.name,
|
||||
investmentModel: inputs.investmentModel,
|
||||
finalInstances: currentInstances,
|
||||
baselineFinalInstances: baselineInstances,
|
||||
totalRevenue,
|
||||
cspRevenue: cumulativeCSPRevenue,
|
||||
servalaRevenue: cumulativeServalaRevenue,
|
||||
netPosition: finalNetPosition,
|
||||
roi,
|
||||
breakEvenMonth,
|
||||
effectiveGracePeriod,
|
||||
avgPerformanceBonus,
|
||||
monthlyData,
|
||||
investmentScaleFactor: investmentScaleFactor,
|
||||
adjustedChurnRate: adjustedChurnRate * 100
|
||||
adjustedChurnRate: adjustedChurnRate * 100,
|
||||
performanceMultiplier: baselineInstances > 0 ? (currentInstances / baselineInstances) : 1.0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error calculating scenario ${scenarioKey}:`, error);
|
||||
|
|
|
|||
|
|
@ -17,26 +17,31 @@ class ChartManager {
|
|||
}
|
||||
|
||||
try {
|
||||
// Instance Growth Chart
|
||||
const instanceCanvas = document.getElementById('instanceGrowthChart');
|
||||
if (!instanceCanvas) {
|
||||
console.error('Instance growth chart canvas not found');
|
||||
// ROI Progression Chart (replaces Instance Growth Chart)
|
||||
const roiCanvas = document.getElementById('instanceGrowthChart');
|
||||
if (!roiCanvas) {
|
||||
console.error('ROI progression chart canvas not found');
|
||||
return;
|
||||
}
|
||||
const instanceCtx = instanceCanvas.getContext('2d');
|
||||
this.charts.instanceGrowth = new Chart(instanceCtx, {
|
||||
const roiCtx = roiCanvas.getContext('2d');
|
||||
this.charts.roiProgression = new Chart(roiCtx, {
|
||||
type: 'line',
|
||||
data: { labels: [], datasets: [] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'top' }
|
||||
legend: { position: 'top' },
|
||||
title: { display: true, text: 'ROI Progression Over Time' }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Total Instances' }
|
||||
title: { display: true, text: 'ROI (%)' },
|
||||
grid: {
|
||||
color: function(context) {
|
||||
return context.tick.value === 0 ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.1)';
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: 'Month' }
|
||||
|
|
@ -45,26 +50,31 @@ class ChartManager {
|
|||
}
|
||||
});
|
||||
|
||||
// Revenue Chart
|
||||
const revenueCanvas = document.getElementById('revenueChart');
|
||||
if (!revenueCanvas) {
|
||||
console.error('Revenue chart canvas not found');
|
||||
// Net Position Chart (replaces simple Revenue Chart)
|
||||
const netPositionCanvas = document.getElementById('revenueChart');
|
||||
if (!netPositionCanvas) {
|
||||
console.error('Net position chart canvas not found');
|
||||
return;
|
||||
}
|
||||
const revenueCtx = revenueCanvas.getContext('2d');
|
||||
this.charts.revenue = new Chart(revenueCtx, {
|
||||
const netPositionCtx = netPositionCanvas.getContext('2d');
|
||||
this.charts.netPosition = new Chart(netPositionCtx, {
|
||||
type: 'line',
|
||||
data: { labels: [], datasets: [] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'top' }
|
||||
legend: { position: 'top' },
|
||||
title: { display: true, text: 'Net Financial Position (Break-Even Analysis)' }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Cumulative Revenue (CHF)' }
|
||||
title: { display: true, text: 'Net Position (CHF)' },
|
||||
grid: {
|
||||
color: function(context) {
|
||||
return context.tick.value === 0 ? 'rgba(0,0,0,0.8)' : 'rgba(0,0,0,0.1)';
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: 'Month' }
|
||||
|
|
@ -73,28 +83,30 @@ class ChartManager {
|
|||
}
|
||||
});
|
||||
|
||||
// Cash Flow Chart
|
||||
const cashFlowCanvas = document.getElementById('cashFlowChart');
|
||||
if (!cashFlowCanvas) {
|
||||
console.error('Cash flow chart canvas not found');
|
||||
// Model Comparison Chart (replaces generic Cash Flow Chart)
|
||||
const modelComparisonCanvas = document.getElementById('cashFlowChart');
|
||||
if (!modelComparisonCanvas) {
|
||||
console.error('Model comparison chart canvas not found');
|
||||
return;
|
||||
}
|
||||
const cashFlowCtx = cashFlowCanvas.getContext('2d');
|
||||
this.charts.cashFlow = new Chart(cashFlowCtx, {
|
||||
const modelComparisonCtx = modelComparisonCanvas.getContext('2d');
|
||||
this.charts.modelComparison = new Chart(modelComparisonCtx, {
|
||||
type: 'bar',
|
||||
data: { labels: [], datasets: [] },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'top' }
|
||||
legend: { position: 'top' },
|
||||
title: { display: true, text: 'Investment Model Performance Comparison' }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
title: { display: true, text: 'Monthly Cash Flow (CHF)' }
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Total Return (CHF)' }
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: 'Month' }
|
||||
title: { display: true, text: 'Growth Scenario' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -126,11 +138,11 @@ class ChartManager {
|
|||
updateCharts() {
|
||||
try {
|
||||
const scenarios = Object.keys(this.calculator.results);
|
||||
if (scenarios.length === 0 || !this.charts.instanceGrowth) return;
|
||||
if (scenarios.length === 0 || !this.charts.roiProgression) return;
|
||||
|
||||
const colors = {
|
||||
conservative: '#28a745',
|
||||
moderate: '#ffc107',
|
||||
moderate: '#ffc107',
|
||||
aggressive: '#dc3545'
|
||||
};
|
||||
|
||||
|
|
@ -138,43 +150,65 @@ class ChartManager {
|
|||
const maxMonths = Math.max(...scenarios.map(s => this.calculator.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.calculator.scenarios[scenario].name,
|
||||
data: this.calculator.monthlyData[scenario].map(d => d.totalInstances),
|
||||
// Update ROI Progression Chart
|
||||
this.charts.roiProgression.data.labels = monthLabels;
|
||||
this.charts.roiProgression.data.datasets = scenarios.map(scenario => ({
|
||||
label: `${this.calculator.scenarios[scenario].name} (${this.calculator.results[scenario].investmentModel})`,
|
||||
data: this.calculator.monthlyData[scenario].map(d => d.roiPercent),
|
||||
borderColor: colors[scenario],
|
||||
backgroundColor: colors[scenario] + '20',
|
||||
tension: 0.4
|
||||
tension: 0.4,
|
||||
pointBackgroundColor: this.calculator.monthlyData[scenario].map(d =>
|
||||
d.roiPercent >= 0 ? colors[scenario] : '#dc3545'
|
||||
)
|
||||
}));
|
||||
this.charts.instanceGrowth.update();
|
||||
this.charts.roiProgression.update();
|
||||
|
||||
// Update Revenue Chart
|
||||
this.charts.revenue.data.labels = monthLabels;
|
||||
this.charts.revenue.data.datasets = scenarios.map(scenario => ({
|
||||
label: this.calculator.scenarios[scenario].name + ' (CSP)',
|
||||
data: this.calculator.monthlyData[scenario].map(d => d.cumulativeCSPRevenue),
|
||||
// Update Net Position Chart (Break-Even Analysis)
|
||||
this.charts.netPosition.data.labels = monthLabels;
|
||||
this.charts.netPosition.data.datasets = scenarios.map(scenario => ({
|
||||
label: `${this.calculator.scenarios[scenario].name} Net Position`,
|
||||
data: this.calculator.monthlyData[scenario].map(d => d.netPosition),
|
||||
borderColor: colors[scenario],
|
||||
backgroundColor: colors[scenario] + '20',
|
||||
tension: 0.4
|
||||
tension: 0.4,
|
||||
fill: {
|
||||
target: 'origin',
|
||||
above: colors[scenario] + '10',
|
||||
below: '#dc354510'
|
||||
}
|
||||
}));
|
||||
this.charts.revenue.update();
|
||||
this.charts.netPosition.update();
|
||||
|
||||
// Update Cash Flow Chart (show average across scenarios)
|
||||
const avgCashFlow = monthLabels.map((_, monthIndex) => {
|
||||
const monthData = scenarios.map(scenario =>
|
||||
this.calculator.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'
|
||||
// Update Model Comparison Chart
|
||||
const scenarioLabels = scenarios.map(s => this.calculator.scenarios[s].name);
|
||||
const currentInvestmentModel = Object.values(this.calculator.results)[0]?.investmentModel || 'direct';
|
||||
|
||||
// Show comparison with both models for the same scenarios
|
||||
this.charts.modelComparison.data.labels = scenarioLabels;
|
||||
this.charts.modelComparison.data.datasets = [{
|
||||
label: `${currentInvestmentModel === 'loan' ? 'Loan Model' : 'Direct Investment'} - Final Return`,
|
||||
data: scenarios.map(scenario => this.calculator.results[scenario].cspRevenue),
|
||||
backgroundColor: scenarios.map(scenario => colors[scenario] + '80'),
|
||||
borderColor: scenarios.map(scenario => colors[scenario]),
|
||||
borderWidth: 2
|
||||
}];
|
||||
this.charts.cashFlow.update();
|
||||
|
||||
// Add performance metrics for direct investment
|
||||
if (currentInvestmentModel === 'direct') {
|
||||
this.charts.modelComparison.data.datasets.push({
|
||||
label: 'Performance Bonus Impact',
|
||||
data: scenarios.map(scenario =>
|
||||
this.calculator.results[scenario].avgPerformanceBonus *
|
||||
this.calculator.results[scenario].cspRevenue
|
||||
),
|
||||
backgroundColor: '#17a2b8',
|
||||
borderColor: '#17a2b8',
|
||||
borderWidth: 2
|
||||
});
|
||||
}
|
||||
|
||||
this.charts.modelComparison.update();
|
||||
} catch (error) {
|
||||
console.error('Error updating charts:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,10 +415,10 @@ class ROICalculatorApp {
|
|||
|
||||
if (selectedModel === 'loan') {
|
||||
if (loanSection) loanSection.style.display = 'block';
|
||||
if (modelDescription) modelDescription.textContent = 'Loan Model: Guaranteed returns with fixed monthly payments';
|
||||
if (modelDescription) modelDescription.textContent = 'Loan Model: 3-7% guaranteed annual returns with predictable monthly payments and low risk';
|
||||
} else {
|
||||
if (loanSection) loanSection.style.display = 'none';
|
||||
if (modelDescription) modelDescription.textContent = 'Direct Investment: Higher potential returns based on your sales performance';
|
||||
if (modelDescription) modelDescription.textContent = 'Direct Investment: Performance-based returns with progressive scaling, bonuses up to 15%, and dynamic grace periods';
|
||||
}
|
||||
|
||||
this.updateCalculations();
|
||||
|
|
|
|||
|
|
@ -11,23 +11,49 @@ class UIManager {
|
|||
try {
|
||||
const enabledResults = Object.values(this.calculator.results);
|
||||
if (enabledResults.length === 0) {
|
||||
this.setElementText('total-instances', '0');
|
||||
this.setElementText('total-revenue', 'CHF 0');
|
||||
this.setElementText('net-position', 'CHF 0');
|
||||
this.setElementText('csp-revenue', 'CHF 0');
|
||||
this.setElementText('roi-percentage', '0%');
|
||||
this.setElementText('breakeven-time', '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;
|
||||
// Calculate averages across enabled scenarios with enhanced financial metrics
|
||||
const avgNetPosition = enabledResults.reduce((sum, r) => sum + (r.netPosition || 0), 0) / enabledResults.length;
|
||||
const avgCSPRevenue = enabledResults.reduce((sum, r) => sum + r.cspRevenue, 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;
|
||||
|
||||
this.setElementText('total-instances', avgInstances.toLocaleString());
|
||||
this.setElementText('total-revenue', this.formatCurrency(avgRevenue));
|
||||
// Update metrics with financial focus
|
||||
this.setElementText('net-position', this.formatCurrency(avgNetPosition));
|
||||
this.setElementText('csp-revenue', this.formatCurrency(avgCSPRevenue));
|
||||
this.setElementText('roi-percentage', this.formatPercentage(avgROI));
|
||||
this.setElementText('breakeven-time', isNaN(avgBreakeven) ? 'N/A' : `${Math.round(avgBreakeven)} months`);
|
||||
|
||||
// Update metric card styling based on performance
|
||||
const netPositionElement = document.getElementById('net-position');
|
||||
if (netPositionElement) {
|
||||
if (avgNetPosition > 0) {
|
||||
netPositionElement.className = 'metric-value text-success';
|
||||
} else if (avgNetPosition < 0) {
|
||||
netPositionElement.className = 'metric-value text-danger';
|
||||
} else {
|
||||
netPositionElement.className = 'metric-value';
|
||||
}
|
||||
}
|
||||
|
||||
const roiElement = document.getElementById('roi-percentage');
|
||||
if (roiElement) {
|
||||
if (avgROI > 15) {
|
||||
roiElement.className = 'metric-value text-success';
|
||||
} else if (avgROI > 5) {
|
||||
roiElement.className = 'metric-value text-warning';
|
||||
} else if (avgROI < 0) {
|
||||
roiElement.className = 'metric-value text-danger';
|
||||
} else {
|
||||
roiElement.className = 'metric-value';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating summary metrics:', error);
|
||||
}
|
||||
|
|
@ -48,15 +74,23 @@ class UIManager {
|
|||
'<span class="badge bg-warning">Loan</span>' :
|
||||
'<span class="badge bg-success">Direct</span>';
|
||||
|
||||
const performanceInfo = result.investmentModel === 'direct' ?
|
||||
`<small class="text-muted d-block">Performance: ${result.performanceMultiplier.toFixed(2)}x</small>` +
|
||||
`<small class="text-muted d-block">Grace: ${result.effectiveGracePeriod} months</small>` :
|
||||
'<small class="text-muted">Fixed returns</small>';
|
||||
|
||||
const row = tbody.insertRow();
|
||||
row.innerHTML = `
|
||||
<td><strong>${result.scenario}</strong></td>
|
||||
<td><strong>${result.scenario}</strong><br>${performanceInfo}</td>
|
||||
<td>${modelLabel}</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 class="${result.roi >= 0 ? 'text-success' : 'text-danger'}">
|
||||
${this.formatPercentage(result.roi)}
|
||||
${result.avgPerformanceBonus > 0 ? `<br><small class="text-info">+${this.formatPercentage(result.avgPerformanceBonus * 100)} bonus</small>` : ''}
|
||||
</td>
|
||||
<td>${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'}</td>
|
||||
`;
|
||||
});
|
||||
|
|
@ -87,16 +121,22 @@ class UIManager {
|
|||
|
||||
allData.forEach(data => {
|
||||
const row = tbody.insertRow();
|
||||
|
||||
// Enhanced monthly breakdown with financial focus
|
||||
const performanceIcon = data.performanceBonus > 0 ? ' <i class="bi bi-star-fill text-warning" title="Performance Bonus Active"></i>' : '';
|
||||
const graceIcon = data.month <= (data.effectiveGracePeriod || 6) ? ' <i class="bi bi-shield-fill-check text-success" title="Grace Period Active"></i>' : '';
|
||||
const netPositionClass = (data.netPosition || 0) >= 0 ? 'text-success' : 'text-danger';
|
||||
|
||||
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><strong>${data.month}</strong>${graceIcon}</td>
|
||||
<td><span class="badge bg-secondary">${data.scenario}</span>${performanceIcon}</td>
|
||||
<td>+${data.newInstances}</td>
|
||||
<td class="text-muted">-${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>
|
||||
<td class="fw-bold">${this.formatCurrencyDetailed(data.cspRevenue)}</td>
|
||||
<td class="text-muted">${this.formatCurrencyDetailed(data.servalaRevenue)}</td>
|
||||
<td class="${netPositionClass} fw-bold">${this.formatCurrencyDetailed(data.netPosition || (data.cumulativeCSPRevenue - 500000))}</td>
|
||||
`;
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue