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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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?')) {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue