166 lines
No EOL
6.6 KiB
JavaScript
166 lines
No EOL
6.6 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('total-instances', '0');
|
|
this.setElementText('total-revenue', 'CHF 0');
|
|
this.setElementText('roi-percentage', '0%');
|
|
this.setElementText('breakeven-time', 'N/A');
|
|
return;
|
|
}
|
|
|
|
// Calculate averages across enabled scenarios
|
|
const avgInstances = Math.round(enabledResults.reduce((sum, r) => sum + r.finalInstances, 0) / enabledResults.length);
|
|
const avgRevenue = enabledResults.reduce((sum, r) => sum + r.totalRevenue, 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;
|
|
|
|
this.setElementText('total-instances', avgInstances.toLocaleString());
|
|
this.setElementText('total-revenue', this.formatCurrency(avgRevenue));
|
|
this.setElementText('roi-percentage', this.formatPercentage(avgROI));
|
|
this.setElementText('breakeven-time', isNaN(avgBreakeven) ? 'N/A' : `${Math.round(avgBreakeven)} months`);
|
|
} 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 row = tbody.insertRow();
|
|
row.innerHTML = `
|
|
<td><strong>${result.scenario}</strong></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)}</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();
|
|
row.innerHTML = `
|
|
<td>${data.month}</td>
|
|
<td><span class="badge bg-secondary">${data.scenario}</span></td>
|
|
<td>${data.newInstances}</td>
|
|
<td>${data.churnedInstances}</td>
|
|
<td>${data.totalInstances.toLocaleString()}</td>
|
|
<td>${this.formatCurrencyDetailed(data.monthlyRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(data.cspRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(data.servalaRevenue)}</td>
|
|
<td>${this.formatCurrencyDetailed(data.cumulativeCSPRevenue)}</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)}%`;
|
|
}
|
|
}
|
|
} |