/** * 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' ? 'Loan' : 'Direct'; const performanceInfo = result.investmentModel === 'direct' ? `Performance: ${result.performanceMultiplier.toFixed(2)}x` + `Grace: ${result.effectiveGracePeriod} months` : 'Fixed returns'; const row = tbody.insertRow(); row.innerHTML = ` ${result.scenario}
${performanceInfo} ${modelLabel} ${result.finalInstances.toLocaleString()} ${this.formatCurrencyDetailed(result.totalRevenue)} ${this.formatCurrencyDetailed(result.cspRevenue)} ${this.formatCurrencyDetailed(result.servalaRevenue)} ${this.formatPercentage(result.roi)} ${result.avgPerformanceBonus > 0 ? `
+${this.formatPercentage(result.avgPerformanceBonus * 100)} bonus` : ''} ${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'} `; }); } 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 ? ' ' : ''; const graceIcon = data.month <= (data.effectiveGracePeriod || 6) ? ' ' : ''; const netPositionClass = (data.netPosition || 0) >= 0 ? 'text-success' : 'text-danger'; row.innerHTML = ` ${data.month}${graceIcon} ${data.scenario}${performanceIcon} +${data.newInstances} -${data.churnedInstances} ${data.totalInstances.toLocaleString()} ${this.formatCurrencyDetailed(data.monthlyRevenue)} ${this.formatCurrencyDetailed(data.cspRevenue)} ${this.formatCurrencyDetailed(data.servalaRevenue)} ${this.formatCurrencyDetailed(data.netPosition || (data.cumulativeCSPRevenue - 500000))} `; }); } 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)}%`; } } }