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 = ` - ${result.scenario}
${performanceInfo} - ${modelLabel} - ${result.finalInstances.toLocaleString()} - ${this.formatCurrencyDetailed(result.totalRevenue)} - ${this.formatCurrencyDetailed(result.cspRevenue)} - ${this.formatCurrencyDetailed(result.servalaRevenue)} - - ${this.formatPercentage(result.roi)} - ${result.avgPerformanceBonus > 0 ? `
+${this.formatPercentage(result.avgPerformanceBonus * 100)} bonus` : ''} + ${result.scenario} + ${modelBadge} + ${result.finalInstances ? result.finalInstances.toLocaleString() : 'N/A'} + + ${this.formatCurrencyDetailed(result.netPosition || 0)} - ${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'N/A'} + + ${this.formatPercentage(result.roi || 0)} + + ${result.breakEvenMonth ? result.breakEvenMonth + ' months' : 'No break-even'} + ${keyFeatures} `; }); } 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 ? ' ' : ''; - const graceIcon = data.month <= (data.effectiveGracePeriod || 6) ? ' ' : ''; + // Model styling + const modelBadge = isDirect ? + 'Direct' : + 'Loan'; + + // Net position styling const netPositionClass = (data.netPosition || 0) >= 0 ? 'text-success' : 'text-danger'; row.innerHTML = ` - ${data.month}${graceIcon} - ${data.scenario}${performanceIcon} - +${data.newInstances} - -${data.churnedInstances} - ${data.totalInstances.toLocaleString()} - ${this.formatCurrencyDetailed(data.monthlyRevenue)} - ${this.formatCurrencyDetailed(data.cspRevenue)} - ${this.formatCurrencyDetailed(data.servalaRevenue)} - ${this.formatCurrencyDetailed(data.netPosition || (data.cumulativeCSPRevenue - 500000))} + ${data.month} + ${data.scenario} + ${modelBadge} + ${data.totalInstances ? data.totalInstances.toLocaleString() : '0'} + ${this.formatCurrencyDetailed(data.monthlyRevenue || 0)} + ${this.formatCurrencyDetailed(data.cspRevenue || 0)} + ${this.formatCurrencyDetailed(data.netPosition || 0)} `; }); } 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'; + } + } } \ No newline at end of file diff --git a/hub/services/templates/calculator/csp_roi_calculator.html b/hub/services/templates/calculator/csp_roi_calculator.html index 31622d8..7e56dd1 100644 --- a/hub/services/templates/calculator/csp_roi_calculator.html +++ b/hub/services/templates/calculator/csp_roi_calculator.html @@ -38,7 +38,7 @@ function resetAdvancedParameters() { window.ROICalculatorApp?.resetAdvancedParam function toggleScenario(scenarioKey) { window.ROICalculatorApp?.toggleScenario(scenarioKey); } function toggleCollapsible(elementId) { window.ROICalculatorApp?.toggleCollapsible(elementId); } function resetCalculator() { window.ROICalculatorApp?.resetCalculator(); } -function toggleInvestmentModel() { window.ROICalculatorApp?.toggleInvestmentModel(); } +// toggleInvestmentModel function removed - both models calculated simultaneously function logout() { window.ROICalculatorApp?.logout(); } // Manual toggle functions for collapse elements @@ -123,100 +123,112 @@ document.addEventListener('DOMContentLoaded', function() { - -
- -
- -
- CHF - -
- -
- -
- - -
- - -
- -
- - - - -
-
- - -
- -
- - CHF -
- -
- - -
- -
-
- - + +
+ +
+ +
+ +
+ CHF +
-
- - + +
+ + +
+
+
+ + +
+
+ +
+ + CHF +
+ +
-
- - +
+ + + +
+
+ + + Help +
- -
- -
- - - Help - -
-
- - -
-
-
-
CHF 0
-
Net Position
+ +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
-
-
0%
-
ROI
+
+ + +
+
+
+
+
Direct Investment
+
CHF 0
+
0%
+
Net Position / ROI
+
+
+
+
+
Loan Model
+
CHF 0
+
0%
+
Net Position / ROI
+
+
@@ -225,8 +237,8 @@ document.addEventListener('DOMContentLoaded', function() {
- -