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

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