diff --git a/hub/services/static/css/roi-calculator.css b/hub/services/static/css/roi-calculator.css
index 6626526..d441efe 100644
--- a/hub/services/static/css/roi-calculator.css
+++ b/hub/services/static/css/roi-calculator.css
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/hub/services/static/js/roi-calculator/calculator-core.js b/hub/services/static/js/roi-calculator/calculator-core.js
index bd3fba4..3a065a1 100644
--- a/hub/services/static/js/roi-calculator/calculator-core.js
+++ b/hub/services/static/js/roi-calculator/calculator-core.js
@@ -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';
}
}
}
\ No newline at end of file
diff --git a/hub/services/static/js/roi-calculator/chart-manager.js b/hub/services/static/js/roi-calculator/chart-manager.js
index 0e8d7e6..5ebac63 100644
--- a/hub/services/static/js/roi-calculator/chart-manager.js
+++ b/hub/services/static/js/roi-calculator/chart-manager.js
@@ -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
}
];
diff --git a/hub/services/static/js/roi-calculator/roi-calculator-app.js b/hub/services/static/js/roi-calculator/roi-calculator-app.js
index b346b0b..6c40b18 100644
--- a/hub/services/static/js/roi-calculator/roi-calculator-app.js
+++ b/hub/services/static/js/roi-calculator/roi-calculator-app.js
@@ -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?')) {
diff --git a/hub/services/static/js/roi-calculator/ui-manager.js b/hub/services/static/js/roi-calculator/ui-manager.js
index 89c1114..c31493c 100644
--- a/hub/services/static/js/roi-calculator/ui-manager.js
+++ b/hub/services/static/js/roi-calculator/ui-manager.js
@@ -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' ?
- 'Loan' :
- 'Direct';
+ // 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' ?
- `Performance: ${result.performanceMultiplier.toFixed(2)}x` +
- `Grace: ${result.effectiveGracePeriod} months` :
- 'Fixed returns';
+ // Model badge with better styling
+ const modelBadge = isDirect ?
+ 'Direct' :
+ 'Loan';
+
+ // Key features based on model
+ const keyFeatures = isDirect ?
+ `Grace period: ${result.effectiveGracePeriod || 6} months
` +
+ `Performance multiplier: ${result.performanceMultiplier ? result.performanceMultiplier.toFixed(2) : '1.0'}x` :
+ `Fixed ${this.formatPercentage(this.calculator.getInputValues().loanInterestRate * 100)} rate
` +
+ 'Predictable returns';
const row = tbody.insertRow();
row.innerHTML = `
-