refactor roi calc js into modular files
This commit is contained in:
parent
51d80364c0
commit
afe3817395
11 changed files with 1611 additions and 1144 deletions
166
hub/services/static/js/roi-calculator/ui-manager.js
Normal file
166
hub/services/static/js/roi-calculator/ui-manager.js
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* 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)}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue