website/hub/services/static/js/roi-calculator/ui-manager.js

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)}%`;
}
}
}