show both models at the same time
This commit is contained in:
parent
aa57082a1b
commit
4746cfac25
6 changed files with 488 additions and 326 deletions
|
|
@ -352,4 +352,147 @@
|
|||
padding: 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -314,17 +314,25 @@ class ROICalculator {
|
|||
|
||||
// Show loading spinner
|
||||
const loadingSpinner = document.getElementById('loading-spinner');
|
||||
const summaryMetrics = document.getElementById('summary-metrics');
|
||||
|
||||
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 => {
|
||||
const result = this.calculateScenario(scenarioKey, inputs);
|
||||
if (result) {
|
||||
this.results[scenarioKey] = result;
|
||||
this.monthlyData[scenarioKey] = result.monthlyData;
|
||||
// Calculate for Direct investment model
|
||||
const directInputs = { ...inputs, investmentModel: 'direct' };
|
||||
const directResult = this.calculateScenario(scenarioKey, directInputs);
|
||||
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
|
||||
if (loadingSpinner) loadingSpinner.style.display = 'none';
|
||||
if (summaryMetrics) summaryMetrics.style.display = 'flex';
|
||||
} catch (error) {
|
||||
console.error('Error updating calculations:', error);
|
||||
// Hide loading spinner on error
|
||||
const loadingSpinner = document.getElementById('loading-spinner');
|
||||
const summaryMetrics = document.getElementById('summary-metrics');
|
||||
if (loadingSpinner) loadingSpinner.style.display = 'none';
|
||||
if (summaryMetrics) summaryMetrics.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,98 +175,98 @@ class ChartManager {
|
|||
aggressive: '#dc3545'
|
||||
};
|
||||
|
||||
const modelColors = {
|
||||
direct: { border: '', background: '80' },
|
||||
loan: { border: '', background: '40' }
|
||||
};
|
||||
|
||||
// Get month labels
|
||||
const maxMonths = Math.max(...scenarios.map(s => this.calculator.monthlyData[s].length));
|
||||
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.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({
|
||||
label: `${this.calculator.scenarios[scenario].name} (${this.calculator.results[scenario].investmentModel})`,
|
||||
data: this.calculator.monthlyData[scenario].map(d => d.roiPercent),
|
||||
borderColor: colors[scenario],
|
||||
backgroundColor: colors[scenario] + '20',
|
||||
tension: 0.4,
|
||||
pointBackgroundColor: this.calculator.monthlyData[scenario].map(d =>
|
||||
d.roiPercent >= 0 ? colors[scenario] : '#dc3545'
|
||||
)
|
||||
}));
|
||||
this.charts.roiProgression.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => {
|
||||
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),
|
||||
borderColor: colors[scenarioBase],
|
||||
backgroundColor: colors[scenarioBase] + (isDirect ? '30' : '15'),
|
||||
borderDash: isDirect ? [] : [5, 5],
|
||||
borderWidth: isDirect ? 3 : 2,
|
||||
tension: 0.4,
|
||||
pointBackgroundColor: this.calculator.monthlyData[scenario].map(d =>
|
||||
d.roiPercent >= 0 ? colors[scenarioBase] : '#dc3545'
|
||||
)
|
||||
};
|
||||
});
|
||||
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.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => ({
|
||||
label: `${this.calculator.scenarios[scenario].name} Net Position`,
|
||||
data: this.calculator.monthlyData[scenario].map(d => d.netPosition),
|
||||
borderColor: colors[scenario],
|
||||
backgroundColor: colors[scenario] + '20',
|
||||
tension: 0.4,
|
||||
fill: {
|
||||
target: 'origin',
|
||||
above: colors[scenario] + '10',
|
||||
below: '#dc354510'
|
||||
}
|
||||
}));
|
||||
this.charts.netPosition.data.datasets = scenarios.filter(s => this.calculator.results[s]).map(scenario => {
|
||||
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),
|
||||
borderColor: colors[scenarioBase],
|
||||
backgroundColor: colors[scenarioBase] + (isDirect ? '30' : '15'),
|
||||
borderDash: isDirect ? [] : [5, 5],
|
||||
borderWidth: isDirect ? 3 : 2,
|
||||
tension: 0.4,
|
||||
fill: {
|
||||
target: 'origin',
|
||||
above: colors[scenarioBase] + (isDirect ? '20' : '10'),
|
||||
below: '#dc354510'
|
||||
}
|
||||
};
|
||||
});
|
||||
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();
|
||||
|
||||
// Calculate loan model net profit (fixed return regardless of scenario)
|
||||
const loanMonthlyPayment = this.calculateLoanPayment(inputs.investmentAmount, inputs.loanInterestRate, inputs.timeframe);
|
||||
const loanTotalPayments = loanMonthlyPayment * (inputs.timeframe * 12);
|
||||
const loanNetProfit = loanTotalPayments - inputs.investmentAmount;
|
||||
// Get unique scenario names (without model suffix)
|
||||
const baseScenarios = ['conservative', 'moderate', 'aggressive'].filter(s =>
|
||||
this.calculator.scenarios[s].enabled
|
||||
);
|
||||
const comparisonLabels = baseScenarios.map(s => this.calculator.scenarios[s].name);
|
||||
|
||||
// Prepare scenario-based comparison
|
||||
const enabledScenarios = scenarios.filter(s => this.calculator.scenarios[s].enabled);
|
||||
const comparisonLabels = enabledScenarios.map(s => this.calculator.scenarios[s].name);
|
||||
|
||||
// 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;
|
||||
// Get net profit data for both models
|
||||
const directInvestmentData = baseScenarios.map(scenario => {
|
||||
const scenarioResult = this.calculator.results[scenario + '_direct'];
|
||||
return scenarioResult ? scenarioResult.netPosition : 0;
|
||||
});
|
||||
|
||||
// Performance bonus data (shows the additional revenue from performance bonuses)
|
||||
const performanceBonusData = enabledScenarios.map(scenario => {
|
||||
const monthlyData = this.calculator.monthlyData[scenario] || [];
|
||||
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;
|
||||
const loanInvestmentData = baseScenarios.map(scenario => {
|
||||
const scenarioResult = this.calculator.results[scenario + '_loan'];
|
||||
return scenarioResult ? scenarioResult.netPosition : 0;
|
||||
});
|
||||
|
||||
this.charts.modelComparison.data.labels = comparisonLabels;
|
||||
this.charts.modelComparison.data.datasets = [
|
||||
{
|
||||
label: `Loan Model (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed return)`,
|
||||
data: loanModelData,
|
||||
backgroundColor: '#ffc107',
|
||||
label: 'Direct Investment Model',
|
||||
data: directInvestmentData,
|
||||
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',
|
||||
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)
|
||||
this.charts.performance.data.labels = comparisonLabels;
|
||||
|
||||
// Calculate ROI for loan model (same for all scenarios)
|
||||
const loanROI = (loanNetProfit / inputs.investmentAmount) * 100;
|
||||
const loanROIData = enabledScenarios.map(() => loanROI);
|
||||
// Get ROI data for both models
|
||||
const directROIData = baseScenarios.map(scenario => {
|
||||
const result = this.calculator.results[scenario + '_direct'];
|
||||
return result ? result.roi : 0;
|
||||
});
|
||||
|
||||
// Get ROI for direct investment (varies by scenario)
|
||||
const directROIData = enabledScenarios.map(scenario => {
|
||||
const result = this.calculator.results[scenario];
|
||||
const loanROIData = baseScenarios.map(scenario => {
|
||||
const result = this.calculator.results[scenario + '_loan'];
|
||||
return result ? result.roi : 0;
|
||||
});
|
||||
|
||||
this.charts.performance.data.datasets = [
|
||||
{
|
||||
label: `Loan Model ROI (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed)`,
|
||||
data: loanROIData,
|
||||
backgroundColor: '#ffc107',
|
||||
borderColor: '#e0a800',
|
||||
label: 'Direct Investment ROI',
|
||||
data: directROIData,
|
||||
backgroundColor: baseScenarios.map(scenario => colors[scenario] + '80'),
|
||||
borderColor: baseScenarios.map(scenario => colors[scenario]),
|
||||
borderWidth: 2
|
||||
},
|
||||
{
|
||||
label: 'Direct Investment ROI',
|
||||
data: directROIData,
|
||||
backgroundColor: enabledScenarios.map(scenario => colors[scenario] + '80'),
|
||||
borderColor: enabledScenarios.map(scenario => colors[scenario]),
|
||||
label: `Loan Model ROI (${(inputs.loanInterestRate * 100).toFixed(1)}% fixed)`,
|
||||
data: loanROIData,
|
||||
backgroundColor: '#ffc10780',
|
||||
borderColor: '#e0a800',
|
||||
borderWidth: 2
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -373,11 +373,7 @@ class ROICalculatorApp {
|
|||
}
|
||||
});
|
||||
|
||||
// Check direct model radio button
|
||||
const directModel = document.getElementById('direct-model');
|
||||
if (directModel) {
|
||||
directModel.checked = true;
|
||||
}
|
||||
// Both models are now calculated simultaneously
|
||||
|
||||
// Reset scenarios
|
||||
['conservative', 'moderate', 'aggressive'].forEach(scenario => {
|
||||
|
|
@ -395,8 +391,7 @@ class ROICalculatorApp {
|
|||
// Reset advanced parameters
|
||||
this.resetAdvancedParameters();
|
||||
|
||||
// Reset investment model toggle
|
||||
this.toggleInvestmentModel();
|
||||
// Both models are now calculated simultaneously, no toggle needed
|
||||
|
||||
// Recalculate
|
||||
this.updateCalculations();
|
||||
|
|
@ -405,27 +400,7 @@ class ROICalculatorApp {
|
|||
}
|
||||
}
|
||||
|
||||
toggleInvestmentModel() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// toggleInvestmentModel removed - both models are now calculated simultaneously
|
||||
|
||||
logout() {
|
||||
if (!confirm('Are you sure you want to logout?')) {
|
||||
|
|
|
|||
|
|
@ -11,47 +11,56 @@ class UIManager {
|
|||
try {
|
||||
const enabledResults = Object.values(this.calculator.results);
|
||||
if (enabledResults.length === 0) {
|
||||
this.setElementText('net-position', 'CHF 0');
|
||||
this.setElementText('csp-revenue', 'CHF 0');
|
||||
this.setElementText('roi-percentage', '0%');
|
||||
this.setElementText('breakeven-time', 'N/A');
|
||||
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;
|
||||
}
|
||||
|
||||
// Calculate averages across enabled scenarios with enhanced financial metrics
|
||||
const avgNetPosition = enabledResults.reduce((sum, r) => sum + (r.netPosition || 0), 0) / enabledResults.length;
|
||||
const avgCSPRevenue = enabledResults.reduce((sum, r) => sum + r.cspRevenue, 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;
|
||||
// Separate direct and loan results
|
||||
const directResults = enabledResults.filter(r => r.investmentModel === 'direct');
|
||||
const loanResults = enabledResults.filter(r => r.investmentModel === 'loan');
|
||||
|
||||
// Update metrics with financial focus
|
||||
this.setElementText('net-position', this.formatCurrency(avgNetPosition));
|
||||
this.setElementText('csp-revenue', this.formatCurrency(avgCSPRevenue));
|
||||
this.setElementText('roi-percentage', this.formatPercentage(avgROI));
|
||||
this.setElementText('breakeven-time', isNaN(avgBreakeven) ? 'N/A' : `${Math.round(avgBreakeven)} months`);
|
||||
|
||||
// Update metric card styling based on performance
|
||||
const netPositionElement = document.getElementById('net-position');
|
||||
if (netPositionElement) {
|
||||
if (avgNetPosition > 0) {
|
||||
netPositionElement.className = 'metric-value text-success';
|
||||
} else if (avgNetPosition < 0) {
|
||||
netPositionElement.className = 'metric-value text-danger';
|
||||
} else {
|
||||
netPositionElement.className = 'metric-value';
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const roiElement = document.getElementById('roi-percentage');
|
||||
if (roiElement) {
|
||||
if (avgROI > 15) {
|
||||
roiElement.className = 'metric-value text-success';
|
||||
} else if (avgROI > 5) {
|
||||
roiElement.className = 'metric-value text-warning';
|
||||
} else if (avgROI < 0) {
|
||||
roiElement.className = 'metric-value text-danger';
|
||||
} else {
|
||||
roiElement.className = 'metric-value';
|
||||
// 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) {
|
||||
|
|
@ -69,29 +78,42 @@ class UIManager {
|
|||
|
||||
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>';
|
||||
// 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);
|
||||
|
||||
const performanceInfo = result.investmentModel === 'direct' ?
|
||||
`<small class="text-muted d-block">Performance: ${result.performanceMultiplier.toFixed(2)}x</small>` +
|
||||
`<small class="text-muted d-block">Grace: ${result.effectiveGracePeriod} months</small>` :
|
||||
'<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();
|
||||
row.innerHTML = `
|
||||
<td><strong>${result.scenario}</strong><br>${performanceInfo}</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)}
|
||||
${result.avgPerformanceBonus > 0 ? `<br><small class="text-info">+${this.formatPercentage(result.avgPerformanceBonus * 100)} bonus</small>` : ''}
|
||||
<td><span class="fw-bold" style="color: ${scenarioColor}">${result.scenario}</span></td>
|
||||
<td>${modelBadge}</td>
|
||||
<td>${result.finalInstances ? result.finalInstances.toLocaleString() : 'N/A'}</td>
|
||||
<td class="fw-bold ${result.netPosition >= 0 ? 'text-success' : 'text-danger'}">
|
||||
${this.formatCurrencyDetailed(result.netPosition || 0)}
|
||||
</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) {
|
||||
|
|
@ -109,34 +131,52 @@ class UIManager {
|
|||
|
||||
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 = [];
|
||||
Object.keys(this.calculator.monthlyData).forEach(scenario => {
|
||||
this.calculator.monthlyData[scenario].forEach(monthData => {
|
||||
allData.push(monthData);
|
||||
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', '');
|
||||
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 => {
|
||||
const row = tbody.insertRow();
|
||||
const scenarioColor = this.getScenarioColor(data.scenario);
|
||||
const isDirect = data.model === 'direct';
|
||||
|
||||
// Enhanced monthly breakdown with financial focus
|
||||
const performanceIcon = data.performanceBonus > 0 ? ' <i class="bi bi-star-fill text-warning" title="Performance Bonus Active"></i>' : '';
|
||||
const graceIcon = data.month <= (data.effectiveGracePeriod || 6) ? ' <i class="bi bi-shield-fill-check text-success" title="Grace Period Active"></i>' : '';
|
||||
// Model styling
|
||||
const modelBadge = isDirect ?
|
||||
'<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';
|
||||
|
||||
row.innerHTML = `
|
||||
<td><strong>${data.month}</strong>${graceIcon}</td>
|
||||
<td><span class="badge bg-secondary">${data.scenario}</span>${performanceIcon}</td>
|
||||
<td>+${data.newInstances}</td>
|
||||
<td class="text-muted">-${data.churnedInstances}</td>
|
||||
<td>${data.totalInstances.toLocaleString()}</td>
|
||||
<td>${this.formatCurrencyDetailed(data.monthlyRevenue)}</td>
|
||||
<td class="fw-bold">${this.formatCurrencyDetailed(data.cspRevenue)}</td>
|
||||
<td class="text-muted">${this.formatCurrencyDetailed(data.servalaRevenue)}</td>
|
||||
<td class="${netPositionClass} fw-bold">${this.formatCurrencyDetailed(data.netPosition || (data.cumulativeCSPRevenue - 500000))}</td>
|
||||
<td class="text-center"><strong>${data.month}</strong></td>
|
||||
<td><span style="color: ${scenarioColor}" class="fw-bold">${data.scenario}</span></td>
|
||||
<td>${modelBadge}</td>
|
||||
<td class="text-end">${data.totalInstances ? data.totalInstances.toLocaleString() : '0'}</td>
|
||||
<td class="text-end">${this.formatCurrencyDetailed(data.monthlyRevenue || 0)}</td>
|
||||
<td class="text-end fw-bold">${this.formatCurrencyDetailed(data.cspRevenue || 0)}</td>
|
||||
<td class="text-end fw-bold ${netPositionClass}">${this.formatCurrencyDetailed(data.netPosition || 0)}</td>
|
||||
`;
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -203,4 +243,14 @@ class UIManager {
|
|||
return `${value.toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getScenarioColor(scenarioName) {
|
||||
switch(scenarioName.toLowerCase()) {
|
||||
case 'conservative': return '#28a745';
|
||||
case 'moderate': return '#ffc107';
|
||||
case 'aggressive': return '#dc3545';
|
||||
default: return '#6c757d';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue