/** * 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-direct', 'CHF 0'); this.setElementText('net-position-loan', 'CHF 0'); this.setElementText('roi-percentage-direct', '0%'); this.setElementText('roi-percentage-loan', '0%'); return; } // Separate direct and loan results const directResults = enabledResults.filter(r => r.investmentModel === 'direct'); const loanResults = enabledResults.filter(r => r.investmentModel === 'loan'); // 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'; } } } // 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) { 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 = ''; // 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); // Model badge with better styling const modelBadge = isDirect ? 'Direct' : 'Loan'; // Key features based on model const keyFeatures = isDirect ? `Grace period: ${result.effectiveGracePeriod || 6} months
` + `Performance multiplier: ${result.performanceMultiplier ? result.performanceMultiplier.toFixed(2) : '1.0'}x` : `Fixed ${this.formatPercentage(this.calculator.getInputValues().loanInterestRate * 100)} rate
` + 'Predictable returns'; const row = tbody.insertRow(); row.innerHTML = ` ${result.scenario} ${modelBadge} ${result.finalInstances ? result.finalInstances.toLocaleString() : 'N/A'} ${this.formatCurrencyDetailed(result.netPosition || 0)} ${this.formatPercentage(result.roi || 0)} ${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'No break-even'} ${keyFeatures} `; }); } 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 = ''; // Get filter settings const filters = this.getMonthlyBreakdownFilters(); // Combine all monthly data and sort by month, then scenario, then model const allData = []; 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', ''); // Apply filters if (!filters.models[model] || !filters.scenarios[scenario.toLowerCase()]) { return; // Skip this entry if filtered out } allData.push({ ...monthData, model: model, scenarioKey: 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'; // Model styling const modelBadge = isDirect ? 'Direct' : 'Loan'; // Net position styling const netPositionClass = (data.netPosition || 0) >= 0 ? 'text-success' : 'text-danger'; row.innerHTML = ` ${data.month} ${data.scenario} ${modelBadge} ${data.totalInstances ? data.totalInstances.toLocaleString() : '0'} ${this.formatCurrencyDetailed(data.serviceRevenue || data.monthlyRevenue || 0)} ${this.formatCurrencyDetailed(data.coreRevenue || 0)} ${this.formatCurrencyDetailed(data.totalRevenue || data.monthlyRevenue || 0)} ${this.formatCurrencyDetailed(data.cspRevenue || 0)} ${this.formatCurrencyDetailed(data.servalaRevenue || 0)} ${this.formatCurrencyDetailed(data.netPosition || 0)} `; }); } 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)}%`; } } getScenarioColor(scenarioName) { switch(scenarioName.toLowerCase()) { case 'conservative': return '#28a745'; case 'moderate': return '#ffc107'; case 'aggressive': return '#dc3545'; default: return '#6c757d'; } } getMonthlyBreakdownFilters() { try { return { models: { direct: document.getElementById('breakdown-direct-enabled')?.checked ?? true, loan: document.getElementById('breakdown-loan-enabled')?.checked ?? true }, scenarios: { conservative: document.getElementById('breakdown-conservative-enabled')?.checked ?? true, moderate: document.getElementById('breakdown-moderate-enabled')?.checked ?? true, aggressive: document.getElementById('breakdown-aggressive-enabled')?.checked ?? true } }; } catch (error) { console.error('Error getting monthly breakdown filters:', error); // Return default filters if there's an error return { models: { direct: true, loan: true }, scenarios: { conservative: true, moderate: true, aggressive: true } }; } } }