show both models at the same time

This commit is contained in:
Tobias Brunner 2025-07-23 11:16:15 +02:00
parent aa57082a1b
commit 4746cfac25
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
6 changed files with 488 additions and 326 deletions

View file

@ -353,3 +353,146 @@
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
} }
/* Clean Dual Model Layout */
.model-comparison-indicator {
font-size: 1.2rem;
color: #6c757d;
}
/* Scenario selection enhancements */
.form-check {
margin-bottom: 0.25rem;
}
.form-check-label {
cursor: pointer;
user-select: none;
}
.form-check-input:checked {
background-color: #0d6efd;
border-color: #0d6efd;
}
/* Model result boxes */
.model-result-box {
transition: all 0.2s ease;
background: #fafafa;
}
.model-result-box:hover {
background: #f0f0f0;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Model comparison box */
.model-comparison-box {
border: 1px solid #dee2e6;
transition: all 0.2s ease;
}
.model-comparison-box:hover {
border-color: #adb5bd;
background-color: #f8f9fa !important;
}
/* Control section styling */
.controls-section {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 8px;
padding: 1rem;
}
/* Enhanced form controls */
.form-range {
height: 4px;
margin-top: 0.5rem;
}
.form-range::-webkit-slider-thumb {
background: #0d6efd;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.form-range::-moz-range-thumb {
background: #0d6efd;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* Button group styling */
.btn-group-enhanced .btn {
border-radius: 4px;
margin-right: 0.25rem;
}
.btn-group-enhanced .btn:last-child {
margin-right: 0;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.controls-section {
padding: 0.75rem;
}
.model-result-box {
margin-bottom: 1rem;
}
}
/* Enhanced table styling for financial analysis */
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.table-success {
--bs-table-bg: rgba(40, 167, 69, 0.1);
}
.table-warning {
--bs-table-bg: rgba(255, 193, 7, 0.1);
}
/* Tab styling improvements */
.nav-tabs .nav-link {
border: 1px solid transparent;
border-top-left-radius: 0.375rem;
border-top-right-radius: 0.375rem;
}
.nav-tabs .nav-link.active {
color: #495057;
background-color: #fff;
border-color: #dee2e6 #dee2e6 #fff;
}
.nav-tabs .nav-link:hover:not(.active) {
border-color: #e9ecef #e9ecef #dee2e6;
isolation: isolate;
}
/* Table cell improvements */
.table td {
vertical-align: middle;
}
.table th {
border-top: none;
font-weight: 600;
font-size: 0.875rem;
}
/* Better responsive tables */
@media (max-width: 768px) {
.table-responsive {
font-size: 0.8rem;
}
.table th, .table td {
padding: 0.5rem 0.25rem;
}
}

View file

@ -314,17 +314,25 @@ class ROICalculator {
// Show loading spinner // Show loading spinner
const loadingSpinner = document.getElementById('loading-spinner'); const loadingSpinner = document.getElementById('loading-spinner');
const summaryMetrics = document.getElementById('summary-metrics');
if (loadingSpinner) loadingSpinner.style.display = 'block'; if (loadingSpinner) loadingSpinner.style.display = 'block';
if (summaryMetrics) summaryMetrics.style.display = 'none';
// Calculate results for each enabled scenario // Calculate results for each enabled scenario with both investment models
Object.keys(this.scenarios).forEach(scenarioKey => { Object.keys(this.scenarios).forEach(scenarioKey => {
const result = this.calculateScenario(scenarioKey, inputs); // Calculate for Direct investment model
if (result) { const directInputs = { ...inputs, investmentModel: 'direct' };
this.results[scenarioKey] = result; const directResult = this.calculateScenario(scenarioKey, directInputs);
this.monthlyData[scenarioKey] = result.monthlyData; if (directResult) {
this.results[scenarioKey + '_direct'] = directResult;
this.monthlyData[scenarioKey + '_direct'] = directResult.monthlyData;
}
// Calculate for Loan investment model
const loanInputs = { ...inputs, investmentModel: 'loan' };
const loanResult = this.calculateScenario(scenarioKey, loanInputs);
if (loanResult) {
this.results[scenarioKey + '_loan'] = loanResult;
this.monthlyData[scenarioKey + '_loan'] = loanResult.monthlyData;
} }
}); });
@ -336,14 +344,11 @@ class ROICalculator {
// Hide loading spinner // Hide loading spinner
if (loadingSpinner) loadingSpinner.style.display = 'none'; if (loadingSpinner) loadingSpinner.style.display = 'none';
if (summaryMetrics) summaryMetrics.style.display = 'flex';
} catch (error) { } catch (error) {
console.error('Error updating calculations:', error); console.error('Error updating calculations:', error);
// Hide loading spinner on error // Hide loading spinner on error
const loadingSpinner = document.getElementById('loading-spinner'); const loadingSpinner = document.getElementById('loading-spinner');
const summaryMetrics = document.getElementById('summary-metrics');
if (loadingSpinner) loadingSpinner.style.display = 'none'; if (loadingSpinner) loadingSpinner.style.display = 'none';
if (summaryMetrics) summaryMetrics.style.display = 'flex';
} }
} }
} }

View file

@ -175,98 +175,98 @@ class ChartManager {
aggressive: '#dc3545' aggressive: '#dc3545'
}; };
const modelColors = {
direct: { border: '', background: '80' },
loan: { border: '', background: '40' }
};
// Get month labels // Get month labels
const maxMonths = Math.max(...scenarios.map(s => this.calculator.monthlyData[s].length)); const maxMonths = Math.max(...scenarios.map(s => this.calculator.monthlyData[s].length));
const monthLabels = Array.from({ length: maxMonths }, (_, i) => `M${i + 1}`); const monthLabels = Array.from({ length: maxMonths }, (_, i) => `M${i + 1}`);
// Update ROI Progression Chart // Update ROI Progression Chart with both models
this.charts.roiProgression.data.labels = monthLabels; this.charts.roiProgression.data.labels = monthLabels;
this.charts.roiProgression.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({ this.charts.roiProgression.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => {
label: `${this.calculator.scenarios[scenario].name} (${this.calculator.results[scenario].investmentModel})`, const scenarioBase = scenario.replace('_direct', '').replace('_loan', '');
const model = scenario.includes('_loan') ? 'loan' : 'direct';
const isDirect = model === 'direct';
const scenarioName = this.calculator.scenarios[scenarioBase]?.name || scenarioBase;
return {
label: `${scenarioName} (${model.charAt(0).toUpperCase() + model.slice(1)})`,
data: this.calculator.monthlyData[scenario].map(d => d.roiPercent), data: this.calculator.monthlyData[scenario].map(d => d.roiPercent),
borderColor: colors[scenario], borderColor: colors[scenarioBase],
backgroundColor: colors[scenario] + '20', backgroundColor: colors[scenarioBase] + (isDirect ? '30' : '15'),
borderDash: isDirect ? [] : [5, 5],
borderWidth: isDirect ? 3 : 2,
tension: 0.4, tension: 0.4,
pointBackgroundColor: this.calculator.monthlyData[scenario].map(d => pointBackgroundColor: this.calculator.monthlyData[scenario].map(d =>
d.roiPercent >= 0 ? colors[scenario] : '#dc3545' d.roiPercent >= 0 ? colors[scenarioBase] : '#dc3545'
) )
})); };
});
this.charts.roiProgression.update(); this.charts.roiProgression.update();
// Update Net Position Chart (Break-Even Analysis) // Update Net Position Chart (Break-Even Analysis) with both models
this.charts.netPosition.data.labels = monthLabels; this.charts.netPosition.data.labels = monthLabels;
this.charts.netPosition.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({ this.charts.netPosition.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => {
label: `${this.calculator.scenarios[scenario].name} Net Position`, const scenarioBase = scenario.replace('_direct', '').replace('_loan', '');
const model = scenario.includes('_loan') ? 'loan' : 'direct';
const isDirect = model === 'direct';
const scenarioName = this.calculator.scenarios[scenarioBase]?.name || scenarioBase;
return {
label: `${scenarioName} (${model.charAt(0).toUpperCase() + model.slice(1)}) Net Position`,
data: this.calculator.monthlyData[scenario].map(d => d.netPosition), data: this.calculator.monthlyData[scenario].map(d => d.netPosition),
borderColor: colors[scenario], borderColor: colors[scenarioBase],
backgroundColor: colors[scenario] + '20', backgroundColor: colors[scenarioBase] + (isDirect ? '30' : '15'),
borderDash: isDirect ? [] : [5, 5],
borderWidth: isDirect ? 3 : 2,
tension: 0.4, tension: 0.4,
fill: { fill: {
target: 'origin', target: 'origin',
above: colors[scenario] + '10', above: colors[scenarioBase] + (isDirect ? '20' : '10'),
below: '#dc354510' below: '#dc354510'
} }
})); };
});
this.charts.netPosition.update(); this.charts.netPosition.update();
// Update Model Comparison Chart - Side-by-side comparison of both models // Update Model Comparison Chart - Direct comparison of both models
const inputs = this.calculator.getInputValues(); const inputs = this.calculator.getInputValues();
// Calculate loan model net profit (fixed return regardless of scenario) // Get unique scenario names (without model suffix)
const loanMonthlyPayment = this.calculateLoanPayment(inputs.investmentAmount, inputs.loanInterestRate, inputs.timeframe); const baseScenarios = ['conservative', 'moderate', 'aggressive'].filter(s =>
const loanTotalPayments = loanMonthlyPayment * (inputs.timeframe * 12); this.calculator.scenarios[s].enabled
const loanNetProfit = loanTotalPayments - inputs.investmentAmount; );
const comparisonLabels = baseScenarios.map(s => this.calculator.scenarios[s].name);
// Prepare scenario-based comparison // Get net profit data for both models
const enabledScenarios = scenarios.filter(s => this.calculator.scenarios[s].enabled); const directInvestmentData = baseScenarios.map(scenario => {
const comparisonLabels = enabledScenarios.map(s => this.calculator.scenarios[s].name); const scenarioResult = this.calculator.results[scenario + '_direct'];
return scenarioResult ? scenarioResult.netPosition : 0;
// Loan model data (same profit for all scenarios since it's fixed)
const loanModelData = enabledScenarios.map(() => loanNetProfit);
// Direct investment data (varies by scenario performance)
const directInvestmentData = enabledScenarios.map(scenario => {
const scenarioResult = this.calculator.results[scenario];
if (!scenarioResult) return 0;
return scenarioResult.cspRevenue - inputs.investmentAmount;
}); });
// Performance bonus data (shows the additional revenue from performance bonuses) const loanInvestmentData = baseScenarios.map(scenario => {
const performanceBonusData = enabledScenarios.map(scenario => { const scenarioResult = this.calculator.results[scenario + '_loan'];
const monthlyData = this.calculator.monthlyData[scenario] || []; return scenarioResult ? scenarioResult.netPosition : 0;
const totalPerformanceBonus = monthlyData.reduce((sum, month) => {
// Calculate the bonus revenue (difference from standard share)
const standardRevenue = month.monthlyRevenue * inputs.servalaShare;
const actualServalaRevenue = month.servalaRevenue;
const bonusAmount = Math.max(0, standardRevenue - actualServalaRevenue); // CSP gets this as bonus
return sum + bonusAmount;
}, 0);
return totalPerformanceBonus;
}); });
this.charts.modelComparison.data.labels = comparisonLabels; this.charts.modelComparison.data.labels = comparisonLabels;
this.charts.modelComparison.data.datasets = [ this.charts.modelComparison.data.datasets = [
{ {
label: `Loan Model (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed return)`, label: 'Direct Investment Model',
data: loanModelData, data: directInvestmentData,
backgroundColor: '#ffc107', backgroundColor: baseScenarios.map(scenario => colors[scenario] + '80'),
borderColor: baseScenarios.map(scenario => colors[scenario]),
borderWidth: 2
},
{
label: `Loan Model (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed rate)`,
data: loanInvestmentData,
backgroundColor: '#ffc10780',
borderColor: '#e0a800', borderColor: '#e0a800',
borderWidth: 2 borderWidth: 2
},
{
label: 'Direct Investment (base return)',
data: directInvestmentData,
backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '80'),
borderColor: enabledScenarios.map(scenario => colors[scenario]),
borderWidth: 2
},
{
label: 'Performance Bonus Impact',
data: performanceBonusData,
backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '40'),
borderColor: enabledScenarios.map(scenario => colors[scenario]),
borderWidth: 2,
borderDash: [5, 5]
} }
]; ];
@ -275,29 +275,30 @@ class ChartManager {
// Update Performance Comparison Chart (ROI comparison for both models) // Update Performance Comparison Chart (ROI comparison for both models)
this.charts.performance.data.labels = comparisonLabels; this.charts.performance.data.labels = comparisonLabels;
// Calculate ROI for loan model (same for all scenarios) // Get ROI data for both models
const loanROI = (loanNetProfit / inputs.investmentAmount) * 100; const directROIData = baseScenarios.map(scenario => {
const loanROIData = enabledScenarios.map(() => loanROI); const result = this.calculator.results[scenario + '_direct'];
return result ? result.roi : 0;
});
// Get ROI for direct investment (varies by scenario) const loanROIData = baseScenarios.map(scenario => {
const directROIData = enabledScenarios.map(scenario => { const result = this.calculator.results[scenario + '_loan'];
const result = this.calculator.results[scenario];
return result ? result.roi : 0; return result ? result.roi : 0;
}); });
this.charts.performance.data.datasets = [ this.charts.performance.data.datasets = [
{ {
label: `Loan Model ROI (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed)`, label: 'Direct Investment ROI',
data: loanROIData, data: directROIData,
backgroundColor: '#ffc107', backgroundColor: baseScenarios.map(scenario => colors[scenario] + '80'),
borderColor: '#e0a800', borderColor: baseScenarios.map(scenario => colors[scenario]),
borderWidth: 2 borderWidth: 2
}, },
{ {
label: 'Direct Investment ROI', label: `Loan Model ROI (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed)`,
data: directROIData, data: loanROIData,
backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '80'), backgroundColor: '#ffc10780',
borderColor: enabledScenarios.map(scenario => colors[scenario]), borderColor: '#e0a800',
borderWidth: 2 borderWidth: 2
} }
]; ];

View file

@ -373,11 +373,7 @@ class ROICalculatorApp {
} }
}); });
// Check direct model radio button // Both models are now calculated simultaneously
const directModel = document.getElementById('direct-model');
if (directModel) {
directModel.checked = true;
}
// Reset scenarios // Reset scenarios
['conservative', 'moderate', 'aggressive'].forEach(scenario => { ['conservative', 'moderate', 'aggressive'].forEach(scenario => {
@ -395,8 +391,7 @@ class ROICalculatorApp {
// Reset advanced parameters // Reset advanced parameters
this.resetAdvancedParameters(); this.resetAdvancedParameters();
// Reset investment model toggle // Both models are now calculated simultaneously, no toggle needed
this.toggleInvestmentModel();
// Recalculate // Recalculate
this.updateCalculations(); this.updateCalculations();
@ -405,27 +400,7 @@ class ROICalculatorApp {
} }
} }
toggleInvestmentModel() { // toggleInvestmentModel removed - both models are now calculated simultaneously
try {
const selectedModelElement = document.querySelector('input[name="investment-model"]:checked');
const selectedModel = selectedModelElement ? selectedModelElement.value : 'direct';
const loanSection = document.getElementById('loan-rate-section');
const modelDescription = document.getElementById('model-description');
if (selectedModel === 'loan') {
if (loanSection) loanSection.style.display = 'block';
if (modelDescription) modelDescription.textContent = 'Loan Model: 3-7% guaranteed annual returns with predictable monthly payments and low risk';
} else {
if (loanSection) loanSection.style.display = 'none';
if (modelDescription) modelDescription.textContent = 'Direct Investment: Performance-based returns with progressive scaling, bonuses up to 15%, and dynamic grace periods';
}
this.updateCalculations();
} catch (error) {
console.error('Error toggling investment model:', error);
}
}
logout() { logout() {
if (!confirm('Are you sure you want to logout?')) { if (!confirm('Are you sure you want to logout?')) {

View file

@ -11,47 +11,56 @@ class UIManager {
try { try {
const enabledResults = Object.values(this.calculator.results); const enabledResults = Object.values(this.calculator.results);
if (enabledResults.length === 0) { if (enabledResults.length === 0) {
this.setElementText('net-position', 'CHF 0'); this.setElementText('net-position-direct', 'CHF 0');
this.setElementText('csp-revenue', 'CHF 0'); this.setElementText('net-position-loan', 'CHF 0');
this.setElementText('roi-percentage', '0%'); this.setElementText('roi-percentage-direct', '0%');
this.setElementText('breakeven-time', 'N/A'); this.setElementText('roi-percentage-loan', '0%');
return; return;
} }
// Calculate averages across enabled scenarios with enhanced financial metrics // Separate direct and loan results
const avgNetPosition = enabledResults.reduce((sum, r) => sum + (r.netPosition || 0), 0) / enabledResults.length; const directResults = enabledResults.filter(r => r.investmentModel === 'direct');
const avgCSPRevenue = enabledResults.reduce((sum, r) => sum + r.cspRevenue, 0) / enabledResults.length; const loanResults = enabledResults.filter(r => r.investmentModel === 'loan');
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 // Calculate averages for direct investment
this.setElementText('net-position', this.formatCurrency(avgNetPosition)); if (directResults.length > 0) {
this.setElementText('csp-revenue', this.formatCurrency(avgCSPRevenue)); const avgNetPositionDirect = directResults.reduce((sum, r) => sum + (r.netPosition || 0), 0) / directResults.length;
this.setElementText('roi-percentage', this.formatPercentage(avgROI)); const avgROIDirect = directResults.reduce((sum, r) => sum + r.roi, 0) / directResults.length;
this.setElementText('breakeven-time', isNaN(avgBreakeven) ? 'N/A' : `${Math.round(avgBreakeven)} months`);
// Update metric card styling based on performance this.setElementText('net-position-direct', this.formatCurrency(avgNetPositionDirect));
const netPositionElement = document.getElementById('net-position'); this.setElementText('roi-percentage-direct', this.formatPercentage(avgROIDirect));
if (netPositionElement) {
if (avgNetPosition > 0) { // Update styling for direct metrics
netPositionElement.className = 'metric-value text-success'; const netPositionDirectElement = document.getElementById('net-position-direct');
} else if (avgNetPosition < 0) { if (netPositionDirectElement) {
netPositionElement.className = 'metric-value text-danger'; if (avgNetPositionDirect > 0) {
netPositionDirectElement.className = 'fw-bold text-success';
} else if (avgNetPositionDirect < 0) {
netPositionDirectElement.className = 'fw-bold text-danger';
} else { } else {
netPositionElement.className = 'metric-value'; netPositionDirectElement.className = 'fw-bold';
}
} }
} }
const roiElement = document.getElementById('roi-percentage'); // Calculate averages for loan investment
if (roiElement) { if (loanResults.length > 0) {
if (avgROI > 15) { const avgNetPositionLoan = loanResults.reduce((sum, r) => sum + (r.netPosition || 0), 0) / loanResults.length;
roiElement.className = 'metric-value text-success'; const avgROILoan = loanResults.reduce((sum, r) => sum + r.roi, 0) / loanResults.length;
} else if (avgROI > 5) {
roiElement.className = 'metric-value text-warning'; this.setElementText('net-position-loan', this.formatCurrency(avgNetPositionLoan));
} else if (avgROI < 0) { this.setElementText('roi-percentage-loan', this.formatPercentage(avgROILoan));
roiElement.className = 'metric-value text-danger';
// 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 { } else {
roiElement.className = 'metric-value'; netPositionLoanElement.className = 'fw-bold';
}
} }
} }
} catch (error) { } catch (error) {
@ -69,29 +78,42 @@ class UIManager {
tbody.innerHTML = ''; tbody.innerHTML = '';
Object.values(this.calculator.results).forEach(result => { // Sort results by scenario first, then by model
const modelLabel = result.investmentModel === 'loan' ? const sortedResults = Object.values(this.calculator.results).sort((a, b) => {
'<span class="badge bg-warning">Loan</span>' : const scenarioCompare = a.scenario.localeCompare(b.scenario);
'<span class="badge bg-success">Direct</span>'; if (scenarioCompare !== 0) return scenarioCompare;
return a.investmentModel.localeCompare(b.investmentModel);
});
const performanceInfo = result.investmentModel === 'direct' ? sortedResults.forEach(result => {
`<small class="text-muted d-block">Performance: ${result.performanceMultiplier.toFixed(2)}x</small>` + const isDirect = result.investmentModel === 'direct';
`<small class="text-muted d-block">Grace: ${result.effectiveGracePeriod} months</small>` : const scenarioColor = this.getScenarioColor(result.scenario);
'<small class="text-muted">Fixed returns</small>';
// Model badge with better styling
const modelBadge = isDirect ?
'<span class="fw-bold text-success">Direct</span>' :
'<span class="fw-bold text-warning">Loan</span>';
// Key features based on model
const keyFeatures = isDirect ?
`Grace period: ${result.effectiveGracePeriod || 6} months<br>` +
`Performance multiplier: ${result.performanceMultiplier ? result.performanceMultiplier.toFixed(2) : '1.0'}x` :
`Fixed ${this.formatPercentage(this.calculator.getInputValues().loanInterestRate * 100)} rate<br>` +
'Predictable returns';
const row = tbody.insertRow(); const row = tbody.insertRow();
row.innerHTML = ` row.innerHTML = `
<td><strong>${result.scenario}</strong><br>${performanceInfo}</td> <td><span class="fw-bold" style="color: ${scenarioColor}">${result.scenario}</span></td>
<td>${modelLabel}</td> <td>${modelBadge}</td>
<td>${result.finalInstances.toLocaleString()}</td> <td>${result.finalInstances ? result.finalInstances.toLocaleString() : 'N/A'}</td>
<td>${this.formatCurrencyDetailed(result.totalRevenue)}</td> <td class="fw-bold ${result.netPosition >= 0 ? 'text-success' : 'text-danger'}">
<td>${this.formatCurrencyDetailed(result.cspRevenue)}</td> ${this.formatCurrencyDetailed(result.netPosition || 0)}
<td>${this.formatCurrencyDetailed(result.servalaRevenue)}</td>
<td class="${result.roi >= 0 ? 'text-success' : 'text-danger'}">
${this.formatPercentage(result.roi)}
${result.avgPerformanceBonus > 0 ? `<br><small class="text-info">+${this.formatPercentage(result.avgPerformanceBonus * 100)} bonus</small>` : ''}
</td> </td>
<td>${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'}</td> <td class="fw-bold ${result.roi >= 15 ? 'text-success' : result.roi >= 5 ? 'text-warning' : result.roi < 0 ? 'text-danger' : ''}">
${this.formatPercentage(result.roi || 0)}
</td>
<td>${result.breakEvenMonth ? result.breakEvenMonth + ' months' : '<span class="text-muted">No break-even</span>'}</td>
<td><small class="text-muted">${keyFeatures}</small></td>
`; `;
}); });
} catch (error) { } catch (error) {
@ -109,34 +131,52 @@ class UIManager {
tbody.innerHTML = ''; tbody.innerHTML = '';
// Combine all monthly data and sort by month and scenario // Combine all monthly data and sort by month, then scenario, then model
const allData = []; const allData = [];
Object.keys(this.calculator.monthlyData).forEach(scenario => { Object.keys(this.calculator.monthlyData).forEach(resultKey => {
this.calculator.monthlyData[scenario].forEach(monthData => { this.calculator.monthlyData[resultKey].forEach(monthData => {
allData.push(monthData); const model = resultKey.includes('_loan') ? 'loan' : 'direct';
const scenario = resultKey.replace('_direct', '').replace('_loan', '');
allData.push({
...monthData,
model: model,
scenarioKey: scenario
});
}); });
}); });
allData.sort((a, b) => a.month - b.month || a.scenario.localeCompare(b.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 => { allData.forEach(data => {
const row = tbody.insertRow(); const row = tbody.insertRow();
const scenarioColor = this.getScenarioColor(data.scenario);
const isDirect = data.model === 'direct';
// Enhanced monthly breakdown with financial focus // Model styling
const performanceIcon = data.performanceBonus > 0 ? ' <i class="bi bi-star-fill text-warning" title="Performance Bonus Active"></i>' : ''; const modelBadge = isDirect ?
const graceIcon = data.month <= (data.effectiveGracePeriod || 6) ? ' <i class="bi bi-shield-fill-check text-success" title="Grace Period Active"></i>' : ''; '<span class="text-success fw-bold">Direct</span>' :
'<span class="text-warning fw-bold">Loan</span>';
// Net position styling
const netPositionClass = (data.netPosition || 0) >= 0 ? 'text-success' : 'text-danger'; const netPositionClass = (data.netPosition || 0) >= 0 ? 'text-success' : 'text-danger';
row.innerHTML = ` row.innerHTML = `
<td><strong>${data.month}</strong>${graceIcon}</td> <td class="text-center"><strong>${data.month}</strong></td>
<td><span class="badge bg-secondary">${data.scenario}</span>${performanceIcon}</td> <td><span style="color: ${scenarioColor}" class="fw-bold">${data.scenario}</span></td>
<td>+${data.newInstances}</td> <td>${modelBadge}</td>
<td class="text-muted">-${data.churnedInstances}</td> <td class="text-end">${data.totalInstances ? data.totalInstances.toLocaleString() : '0'}</td>
<td>${data.totalInstances.toLocaleString()}</td> <td class="text-end">${this.formatCurrencyDetailed(data.monthlyRevenue || 0)}</td>
<td>${this.formatCurrencyDetailed(data.monthlyRevenue)}</td> <td class="text-end fw-bold">${this.formatCurrencyDetailed(data.cspRevenue || 0)}</td>
<td class="fw-bold">${this.formatCurrencyDetailed(data.cspRevenue)}</td> <td class="text-end fw-bold ${netPositionClass}">${this.formatCurrencyDetailed(data.netPosition || 0)}</td>
<td class="text-muted">${this.formatCurrencyDetailed(data.servalaRevenue)}</td>
<td class="${netPositionClass} fw-bold">${this.formatCurrencyDetailed(data.netPosition || (data.cumulativeCSPRevenue - 500000))}</td>
`; `;
}); });
} catch (error) { } catch (error) {
@ -203,4 +243,14 @@ class UIManager {
return `${value.toFixed(1)}%`; return `${value.toFixed(1)}%`;
} }
} }
getScenarioColor(scenarioName) {
switch(scenarioName.toLowerCase()) {
case 'conservative': return '#28a745';
case 'moderate': return '#ffc107';
case 'aggressive': return '#dc3545';
default: return '#6c757d';
}
}
} }

View file

@ -38,7 +38,7 @@ function resetAdvancedParameters() { window.ROICalculatorApp?.resetAdvancedParam
function toggleScenario(scenarioKey) { window.ROICalculatorApp?.toggleScenario(scenarioKey); } function toggleScenario(scenarioKey) { window.ROICalculatorApp?.toggleScenario(scenarioKey); }
function toggleCollapsible(elementId) { window.ROICalculatorApp?.toggleCollapsible(elementId); } function toggleCollapsible(elementId) { window.ROICalculatorApp?.toggleCollapsible(elementId); }
function resetCalculator() { window.ROICalculatorApp?.resetCalculator(); } function resetCalculator() { window.ROICalculatorApp?.resetCalculator(); }
function toggleInvestmentModel() { window.ROICalculatorApp?.toggleInvestmentModel(); } // toggleInvestmentModel function removed - both models calculated simultaneously
function logout() { window.ROICalculatorApp?.logout(); } function logout() { window.ROICalculatorApp?.logout(); }
// Manual toggle functions for collapse elements // Manual toggle functions for collapse elements
@ -123,11 +123,13 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
</div> </div>
<!-- Compact Controls Row --> <!-- Organized Controls Section -->
<div class="row py-3"> <div class="py-3">
<!-- Core Investment Parameters --> <!-- Primary Controls Row -->
<div class="col-lg-2 col-md-3 mb-2"> <div class="row align-items-end mb-3">
<label class="form-label small fw-semibold mb-1">Investment</label> <!-- Investment Amount -->
<div class="col-lg-3 col-md-4 mb-2">
<label class="form-label small fw-semibold mb-1">Initial Investment</label>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-text">CHF</span> <span class="input-group-text">CHF</span>
<input type="text" class="form-control" id="investment-amount" <input type="text" class="form-control" id="investment-amount"
@ -140,7 +142,10 @@ document.addEventListener('DOMContentLoaded', function() {
onchange="updateInvestmentAmount(this.value)"> onchange="updateInvestmentAmount(this.value)">
</div> </div>
<div class="col-lg-1 col-md-2 mb-2"> <!-- Time & Revenue -->
<div class="col-lg-3 col-md-4 mb-2">
<div class="row">
<div class="col-6">
<label class="form-label small fw-semibold mb-1">Years</label> <label class="form-label small fw-semibold mb-1">Years</label>
<select class="form-select form-select-sm" id="timeframe" onchange="updateCalculations()"> <select class="form-select form-select-sm" id="timeframe" onchange="updateCalculations()">
<option value="1">1</option> <option value="1">1</option>
@ -150,21 +155,8 @@ document.addEventListener('DOMContentLoaded', function() {
<option value="5">5</option> <option value="5">5</option>
</select> </select>
</div> </div>
<div class="col-6">
<!-- Investment Model --> <label class="form-label small fw-semibold mb-1">Revenue/Instance</label>
<div class="col-lg-2 col-md-3 mb-2">
<label class="form-label small fw-semibold mb-1">Model</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="investment-model" id="loan-model" value="loan" onchange="toggleInvestmentModel()">
<label class="btn btn-outline-warning btn-sm" for="loan-model">Loan</label>
<input type="radio" class="btn-check" name="investment-model" id="direct-model" value="direct" checked onchange="toggleInvestmentModel()">
<label class="btn btn-outline-success btn-sm" for="direct-model">Direct</label>
</div>
</div>
<!-- Revenue/Instance -->
<div class="col-lg-1 col-md-2 mb-2">
<label class="form-label small fw-semibold mb-1">Revenue</label>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="number" class="form-control" id="revenue-per-instance" <input type="number" class="form-control" id="revenue-per-instance"
min="20" max="200" step="5" value="50" onchange="updateCalculations()"> min="20" max="200" step="5" value="50" onchange="updateCalculations()">
@ -174,49 +166,69 @@ document.addEventListener('DOMContentLoaded', function() {
min="20" max="200" step="5" value="50" min="20" max="200" step="5" value="50"
onchange="updateRevenuePerInstance(this.value)"> onchange="updateRevenuePerInstance(this.value)">
</div> </div>
<!-- Scenarios Toggle -->
<div class="col-lg-2 col-md-3 mb-2">
<label class="form-label small fw-semibold mb-1">Scenarios</label>
<div class="d-flex gap-1">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="conservative-enabled" checked onchange="toggleScenario('conservative')">
<label class="form-check-label small text-success" for="conservative-enabled" data-bs-toggle="tooltip" title="Conservative: 2% churn, steady growth">Safe</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="moderate-enabled" checked onchange="toggleScenario('moderate')">
<label class="form-check-label small text-warning" for="moderate-enabled" data-bs-toggle="tooltip" title="Moderate: 3% churn, balanced growth">Balanced</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="aggressive-enabled" checked onchange="toggleScenario('aggressive')">
<label class="form-check-label small text-danger" for="aggressive-enabled" data-bs-toggle="tooltip" title="Aggressive: 5% churn, rapid growth">Fast</label>
</div>
</div> </div>
</div> </div>
<!-- Advanced Controls Toggle -->
<div class="col-lg-2 col-md-3 mb-2"> <!-- Actions -->
<label class="form-label small fw-semibold mb-1">Controls</label> <div class="col-lg-2 col-md-4 mb-2">
<div class="d-flex gap-1"> <div class="d-flex justify-content-end gap-2">
<button class="btn btn-outline-info btn-sm" type="button" onclick="toggleAdvancedControls()" id="advancedToggleBtn"> <button class="btn btn-outline-info btn-sm" type="button" onclick="toggleAdvancedControls()" id="advancedToggleBtn">
<i class="bi bi-gear"></i> More <i class="bi bi-gear"></i> Advanced
</button> </button>
<a href="{% url 'services:roi_calculator_help' %}" class="btn btn-outline-secondary btn-sm" target="_blank"> <a href="{% url 'services:roi_calculator_help' %}" class="btn btn-outline-secondary btn-sm" target="_blank">
<i class="bi bi-question-circle"></i> Help <i class="bi bi-question-circle"></i> Help
</a> </a>
</div> </div>
</div> </div>
<!-- Remaining space for metrics -->
<div class="col-lg-2 col-md-4 mb-2 text-end">
<div class="d-flex justify-content-end gap-3">
<div class="text-center">
<div class="fw-bold text-success" id="net-position" style="font-size: 1.1rem; white-space: nowrap; line-height: 1.2;">CHF 0</div>
<div style="font-size: 0.8rem;" class="text-muted">Net Position</div>
</div> </div>
<div class="text-center">
<div class="fw-bold text-primary" id="roi-percentage" style="font-size: 1.1rem; white-space: nowrap; line-height: 1.2;">0%</div> <!-- Scenarios & Results Row -->
<div style="font-size: 0.8rem;" class="text-muted">ROI</div> <div class="row align-items-center">
<!-- Scenario Selection -->
<div class="col-lg-6 col-md-8 mb-2">
<label class="form-label small fw-semibold mb-2">Growth Scenarios</label>
<div class="d-flex gap-3 justify-content-center">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="conservative-enabled" checked onchange="toggleScenario('conservative')">
<label class="form-check-label small fw-medium" for="conservative-enabled" data-bs-toggle="tooltip" title="Conservative: 2% churn, steady growth">
<span class="text-success"></span> Conservative
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="moderate-enabled" checked onchange="toggleScenario('moderate')">
<label class="form-check-label small fw-medium" for="moderate-enabled" data-bs-toggle="tooltip" title="Moderate: 3% churn, balanced growth">
<span class="text-warning"></span> Moderate
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="aggressive-enabled" checked onchange="toggleScenario('aggressive')">
<label class="form-check-label small fw-medium" for="aggressive-enabled" data-bs-toggle="tooltip" title="Aggressive: 5% churn, rapid growth">
<span class="text-danger"></span> Aggressive
</label>
</div>
</div>
</div>
<!-- Model Results -->
<div class="col-lg-6 col-md-4 mb-2">
<div class="row">
<div class="col-6">
<div class="text-center p-2 border rounded model-result-box">
<div class="text-success fw-semibold small mb-1">Direct Investment</div>
<div class="fw-bold" id="net-position-direct" style="font-size: 1.1rem; line-height: 1.2;">CHF 0</div>
<div class="fw-bold text-primary" id="roi-percentage-direct" style="font-size: 0.9rem; line-height: 1.2;">0%</div>
<div style="font-size: 0.7rem;" class="text-muted">Net Position / ROI</div>
</div>
</div>
<div class="col-6">
<div class="text-center p-2 border rounded model-result-box">
<div class="text-warning fw-semibold small mb-1">Loan Model</div>
<div class="fw-bold" id="net-position-loan" style="font-size: 1.1rem; line-height: 1.2;">CHF 0</div>
<div class="fw-bold text-primary" id="roi-percentage-loan" style="font-size: 0.9rem; line-height: 1.2;">0%</div>
<div style="font-size: 0.7rem;" class="text-muted">Net Position / ROI</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -225,8 +237,8 @@ document.addEventListener('DOMContentLoaded', function() {
<!-- Collapsible Advanced Controls --> <!-- Collapsible Advanced Controls -->
<div class="collapse" id="advancedControls"> <div class="collapse" id="advancedControls">
<div class="row py-2 bg-white border-top"> <div class="row py-2 bg-white border-top">
<!-- Loan Rate (conditional) --> <!-- Loan Rate (for loan model calculations) -->
<div class="col-md-2" id="loan-rate-section" style="display: none;"> <div class="col-md-2" id="loan-rate-section">
<label class="form-label small mb-1">Loan Rate (%)</label> <label class="form-label small mb-1">Loan Rate (%)</label>
<input type="number" class="form-control form-control-sm" id="loan-interest-rate" <input type="number" class="form-control form-control-sm" id="loan-interest-rate"
min="3" max="8" step="0.1" value="5.0" onchange="updateCalculations()"> min="3" max="8" step="0.1" value="5.0" onchange="updateCalculations()">
@ -288,33 +300,6 @@ document.addEventListener('DOMContentLoaded', function() {
<p class="mt-2">Calculating scenarios...</p> <p class="mt-2">Calculating scenarios...</p>
</div> </div>
<!-- Additional Key Metrics (Horizontal) -->
<div class="row mb-3" id="summary-metrics">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body py-3">
<div class="row text-center">
<div class="col-lg-3 col-md-6 mb-2">
<div class="h5 mb-0" id="csp-revenue">CHF 0</div>
<div class="small text-muted">Your Total Revenue</div>
</div>
<div class="col-lg-3 col-md-6 mb-2">
<div class="h5 mb-0" id="breakeven-time">N/A</div>
<div class="small text-muted">Break-Even Time</div>
</div>
<div class="col-lg-3 col-md-6 mb-2">
<div class="h5 mb-0 text-info" id="model-description-display">Direct Investment</div>
<div class="small text-muted">Investment Model</div>
</div>
<div class="col-lg-3 col-md-6 mb-2">
<div class="h5 mb-0 text-secondary">3 Scenarios</div>
<div class="small text-muted">Active Comparisons</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- PRIMARY CHART - Full Width, Large Height --> <!-- PRIMARY CHART - Full Width, Large Height -->
<div class="row mb-4"> <div class="row mb-4">
@ -380,25 +365,23 @@ document.addEventListener('DOMContentLoaded', function() {
<h2 class="accordion-header" id="dataHeading"> <h2 class="accordion-header" id="dataHeading">
<button class="accordion-button collapsed" type="button" onclick="toggleDataCollapse()" id="dataToggleBtn"> <button class="accordion-button collapsed" type="button" onclick="toggleDataCollapse()" id="dataToggleBtn">
<i class="bi bi-table me-2"></i> Detailed Financial Analysis <i class="bi bi-table me-2"></i> Detailed Financial Analysis
<span class="badge bg-secondary ms-2">Optional</span>
</button> </button>
</h2> </h2>
<div id="dataCollapse" class="accordion-collapse collapse" aria-labelledby="dataHeading" data-bs-parent="#dataAccordion"> <div id="dataCollapse" class="accordion-collapse collapse" aria-labelledby="dataHeading" data-bs-parent="#dataAccordion">
<div class="accordion-body"> <div class="accordion-body">
<!-- Comparison Table --> <!-- Improved Scenario Performance Summary -->
<h6 class="mb-3">Scenario Performance Summary</h6> <h6 class="mb-3">Investment Model Comparison by Scenario</h6>
<div class="table-responsive mb-4"> <div class="table-responsive mb-4">
<table class="table table-sm table-striped" id="comparison-table"> <table class="table table-sm table-hover" id="comparison-table">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th>Scenario</th> <th>Scenario</th>
<th>Model</th> <th>Investment Model</th>
<th>Final Scale</th> <th>Final Scale</th>
<th>Total Revenue</th> <th>Your Net Profit</th>
<th>Your Revenue</th> <th>Total ROI</th>
<th>Servala Share</th> <th>Break-even Time</th>
<th>ROI & Bonuses</th> <th>Key Features</th>
<th>Break-even</th>
</tr> </tr>
</thead> </thead>
<tbody id="comparison-tbody"> <tbody id="comparison-tbody">
@ -407,21 +390,26 @@ document.addEventListener('DOMContentLoaded', function() {
</table> </table>
</div> </div>
<!-- Monthly Breakdown --> <!-- Monthly Financial Flow -->
<h6 class="mb-3">Monthly Financial Flow</h6> <h6 class="mb-3">Monthly Financial Breakdown</h6>
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;"> <div class="alert alert-info alert-sm">
<small>
<i class="bi bi-info-circle"></i>
This table shows month-by-month progression for all enabled scenarios and both investment models.
Use the scenario checkboxes above to filter results.
</small>
</div>
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
<table class="table table-sm table-striped" id="monthly-table"> <table class="table table-sm table-striped" id="monthly-table">
<thead class="table-dark sticky-top"> <thead class="table-dark sticky-top">
<tr> <tr>
<th>Month</th> <th>Month</th>
<th>Scenario</th> <th>Scenario</th>
<th>Growth</th> <th>Model</th>
<th>Churn</th> <th>Instances</th>
<th>Scale</th>
<th>Monthly Revenue</th> <th>Monthly Revenue</th>
<th>Your Share</th> <th>Your Share</th>
<th>Servala Share</th> <th>Cumulative Net Position</th>
<th>Net Position</th>
</tr> </tr>
</thead> </thead>
<tbody id="monthly-tbody"> <tbody id="monthly-tbody">