206 lines
No EOL
9.1 KiB
JavaScript
206 lines
No EOL
9.1 KiB
JavaScript
/**
|
|
* UI Management Module
|
|
* Handles DOM updates, table rendering, and metric display
|
|
*/
|
|
class UIManager {
|
|
constructor(calculator) {
|
|
this.calculator = calculator;
|
|
}
|
|
|
|
updateSummaryMetrics() {
|
|
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');
|
|
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;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
updateComparisonTable() {
|
|
try {
|
|
const tbody = document.getElementById('comparison-tbody');
|
|
if (!tbody) {
|
|
console.error('Comparison table body not found');
|
|
return;
|
|
}
|
|
|
|
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>';
|
|
|
|
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><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>
|
|
<td>${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'}</td>
|
|
`;
|
|
});
|
|
} catch (error) {
|
|
console.error('Error updating comparison table:', error);
|
|
}
|
|
}
|
|
|
|
updateMonthlyBreakdown() {
|
|
try {
|
|
const tbody = document.getElementById('monthly-tbody');
|
|
if (!tbody) {
|
|
console.error('Monthly breakdown table body not found');
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
// Combine all monthly data and sort by month and scenario
|
|
const allData = [];
|
|
Object.keys(this.calculator.monthlyData).forEach(scenario => {
|
|
this.calculator.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();
|
|
|
|
// 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><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>
|
|
`;
|
|
});
|
|
} catch (error) {
|
|
console.error('Error updating monthly breakdown:', error);
|
|
}
|
|
}
|
|
|
|
setElementText(elementId, text) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
element.textContent = text;
|
|
}
|
|
}
|
|
|
|
formatCurrency(amount) {
|
|
try {
|
|
// 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);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error formatting currency:', error);
|
|
return `CHF ${amount.toFixed(0)}`;
|
|
}
|
|
}
|
|
|
|
formatCurrencyDetailed(amount) {
|
|
try {
|
|
// Use full formatting for detailed views (tables, exports)
|
|
return new Intl.NumberFormat('de-CH', {
|
|
style: 'currency',
|
|
currency: 'CHF',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(amount);
|
|
} catch (error) {
|
|
console.error('Error formatting detailed currency:', error);
|
|
return `CHF ${amount.toFixed(0)}`;
|
|
}
|
|
}
|
|
|
|
formatPercentage(value) {
|
|
try {
|
|
return new Intl.NumberFormat('de-CH', {
|
|
style: 'percent',
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1
|
|
}).format(value / 100);
|
|
} catch (error) {
|
|
console.error('Error formatting percentage:', error);
|
|
return `${value.toFixed(1)}%`;
|
|
}
|
|
}
|
|
} |