rework investment model

This commit is contained in:
Tobias Brunner 2025-07-22 09:16:40 +02:00
parent 22bea2c53d
commit 6f6c80480f
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
6 changed files with 633 additions and 141 deletions

View file

@ -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) {