show both models at the same time
This commit is contained in:
parent
aa57082a1b
commit
4746cfac25
6 changed files with 488 additions and 326 deletions
|
|
@ -11,47 +11,56 @@ class UIManager {
|
|||
try {
|
||||
const enabledResults = Object.values(this.calculator.results);
|
||||
if (enabledResults.length === 0) {
|
||||
this.setElementText('net-position', 'CHF 0');
|
||||
this.setElementText('csp-revenue', 'CHF 0');
|
||||
this.setElementText('roi-percentage', '0%');
|
||||
this.setElementText('breakeven-time', 'N/A');
|
||||
this.setElementText('net-position-direct', 'CHF 0');
|
||||
this.setElementText('net-position-loan', 'CHF 0');
|
||||
this.setElementText('roi-percentage-direct', '0%');
|
||||
this.setElementText('roi-percentage-loan', '0%');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Separate direct and loan results
|
||||
const directResults = enabledResults.filter(r => r.investmentModel === 'direct');
|
||||
const loanResults = enabledResults.filter(r => r.investmentModel === 'loan');
|
||||
|
||||
// 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';
|
||||
// Calculate averages for direct investment
|
||||
if (directResults.length > 0) {
|
||||
const avgNetPositionDirect = directResults.reduce((sum, r) => sum + (r.netPosition || 0), 0) / directResults.length;
|
||||
const avgROIDirect = directResults.reduce((sum, r) => sum + r.roi, 0) / directResults.length;
|
||||
|
||||
this.setElementText('net-position-direct', this.formatCurrency(avgNetPositionDirect));
|
||||
this.setElementText('roi-percentage-direct', this.formatPercentage(avgROIDirect));
|
||||
|
||||
// Update styling for direct metrics
|
||||
const netPositionDirectElement = document.getElementById('net-position-direct');
|
||||
if (netPositionDirectElement) {
|
||||
if (avgNetPositionDirect > 0) {
|
||||
netPositionDirectElement.className = 'fw-bold text-success';
|
||||
} else if (avgNetPositionDirect < 0) {
|
||||
netPositionDirectElement.className = 'fw-bold text-danger';
|
||||
} else {
|
||||
netPositionDirectElement.className = 'fw-bold';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
// Calculate averages for loan investment
|
||||
if (loanResults.length > 0) {
|
||||
const avgNetPositionLoan = loanResults.reduce((sum, r) => sum + (r.netPosition || 0), 0) / loanResults.length;
|
||||
const avgROILoan = loanResults.reduce((sum, r) => sum + r.roi, 0) / loanResults.length;
|
||||
|
||||
this.setElementText('net-position-loan', this.formatCurrency(avgNetPositionLoan));
|
||||
this.setElementText('roi-percentage-loan', this.formatPercentage(avgROILoan));
|
||||
|
||||
// Update styling for loan metrics
|
||||
const netPositionLoanElement = document.getElementById('net-position-loan');
|
||||
if (netPositionLoanElement) {
|
||||
if (avgNetPositionLoan > 0) {
|
||||
netPositionLoanElement.className = 'fw-bold text-success';
|
||||
} else if (avgNetPositionLoan < 0) {
|
||||
netPositionLoanElement.className = 'fw-bold text-danger';
|
||||
} else {
|
||||
netPositionLoanElement.className = 'fw-bold';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -69,29 +78,42 @@ class UIManager {
|
|||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
Object.values(this.calculator.results).forEach(result => {
|
||||
const modelLabel = result.investmentModel === 'loan' ?
|
||||
'<span class="badge bg-warning">Loan</span>' :
|
||||
'<span class="badge bg-success">Direct</span>';
|
||||
// Sort results by scenario first, then by model
|
||||
const sortedResults = Object.values(this.calculator.results).sort((a, b) => {
|
||||
const scenarioCompare = a.scenario.localeCompare(b.scenario);
|
||||
if (scenarioCompare !== 0) return scenarioCompare;
|
||||
return a.investmentModel.localeCompare(b.investmentModel);
|
||||
});
|
||||
|
||||
sortedResults.forEach(result => {
|
||||
const isDirect = result.investmentModel === 'direct';
|
||||
const scenarioColor = this.getScenarioColor(result.scenario);
|
||||
|
||||
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>';
|
||||
// Model badge with better styling
|
||||
const modelBadge = isDirect ?
|
||||
'<span class="fw-bold text-success">Direct</span>' :
|
||||
'<span class="fw-bold text-warning">Loan</span>';
|
||||
|
||||
// Key features based on model
|
||||
const keyFeatures = isDirect ?
|
||||
`Grace period: ${result.effectiveGracePeriod || 6} months<br>` +
|
||||
`Performance multiplier: ${result.performanceMultiplier ? result.performanceMultiplier.toFixed(2) : '1.0'}x` :
|
||||
`Fixed ${this.formatPercentage(this.calculator.getInputValues().loanInterestRate * 100)} rate<br>` +
|
||||
'Predictable returns';
|
||||
|
||||
const row = tbody.insertRow();
|
||||
row.innerHTML = `
|
||||
<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)}
|
||||
${result.avgPerformanceBonus > 0 ? `<br><small class="text-info">+${this.formatPercentage(result.avgPerformanceBonus * 100)} bonus</small>` : ''}
|
||||
<td><span class="fw-bold" style="color: ${scenarioColor}">${result.scenario}</span></td>
|
||||
<td>${modelBadge}</td>
|
||||
<td>${result.finalInstances ? result.finalInstances.toLocaleString() : 'N/A'}</td>
|
||||
<td class="fw-bold ${result.netPosition >= 0 ? 'text-success' : 'text-danger'}">
|
||||
${this.formatCurrencyDetailed(result.netPosition || 0)}
|
||||
</td>
|
||||
<td>${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'}</td>
|
||||
<td class="fw-bold ${result.roi >= 15 ? 'text-success' : result.roi >= 5 ? 'text-warning' : result.roi < 0 ? 'text-danger' : ''}">
|
||||
${this.formatPercentage(result.roi || 0)}
|
||||
</td>
|
||||
<td>${result.breakEvenMonth ? result.breakEvenMonth + ' months' : '<span class="text-muted">No break-even</span>'}</td>
|
||||
<td><small class="text-muted">${keyFeatures}</small></td>
|
||||
`;
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -109,34 +131,52 @@ class UIManager {
|
|||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Combine all monthly data and sort by month and scenario
|
||||
// Combine all monthly data and sort by month, then scenario, then model
|
||||
const allData = [];
|
||||
Object.keys(this.calculator.monthlyData).forEach(scenario => {
|
||||
this.calculator.monthlyData[scenario].forEach(monthData => {
|
||||
allData.push(monthData);
|
||||
Object.keys(this.calculator.monthlyData).forEach(resultKey => {
|
||||
this.calculator.monthlyData[resultKey].forEach(monthData => {
|
||||
const model = resultKey.includes('_loan') ? 'loan' : 'direct';
|
||||
const scenario = resultKey.replace('_direct', '').replace('_loan', '');
|
||||
allData.push({
|
||||
...monthData,
|
||||
model: model,
|
||||
scenarioKey: scenario
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
allData.sort((a, b) => a.month - b.month || a.scenario.localeCompare(b.scenario));
|
||||
// Sort by month first, then scenario, then model (direct first)
|
||||
allData.sort((a, b) => {
|
||||
const monthCompare = a.month - b.month;
|
||||
if (monthCompare !== 0) return monthCompare;
|
||||
|
||||
const scenarioCompare = a.scenarioKey.localeCompare(b.scenarioKey);
|
||||
if (scenarioCompare !== 0) return scenarioCompare;
|
||||
|
||||
return a.model === 'direct' ? -1 : 1; // Direct first
|
||||
});
|
||||
|
||||
allData.forEach(data => {
|
||||
const row = tbody.insertRow();
|
||||
const scenarioColor = this.getScenarioColor(data.scenario);
|
||||
const isDirect = data.model === 'direct';
|
||||
|
||||
// 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>' : '';
|
||||
// Model styling
|
||||
const modelBadge = isDirect ?
|
||||
'<span class="text-success fw-bold">Direct</span>' :
|
||||
'<span class="text-warning fw-bold">Loan</span>';
|
||||
|
||||
// Net position styling
|
||||
const netPositionClass = (data.netPosition || 0) >= 0 ? 'text-success' : 'text-danger';
|
||||
|
||||
row.innerHTML = `
|
||||
<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 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>
|
||||
<td class="text-center"><strong>${data.month}</strong></td>
|
||||
<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 fw-bold">${this.formatCurrencyDetailed(data.cspRevenue || 0)}</td>
|
||||
<td class="text-end fw-bold ${netPositionClass}">${this.formatCurrencyDetailed(data.netPosition || 0)}</td>
|
||||
`;
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -203,4 +243,14 @@ class UIManager {
|
|||
return `${value.toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getScenarioColor(scenarioName) {
|
||||
switch(scenarioName.toLowerCase()) {
|
||||
case 'conservative': return '#28a745';
|
||||
case 'moderate': return '#ffc107';
|
||||
case 'aggressive': return '#dc3545';
|
||||
default: return '#6c757d';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue