634 lines
No EOL
24 KiB
JavaScript
634 lines
No EOL
24 KiB
JavaScript
/**
|
|
* ROI Calculator Application
|
|
* Main application class that coordinates all modules
|
|
*/
|
|
class ROICalculatorApp {
|
|
constructor() {
|
|
this.calculator = null;
|
|
this.chartManager = null;
|
|
this.uiManager = null;
|
|
this.exportManager = null;
|
|
this.isInitialized = false;
|
|
}
|
|
|
|
async initialize() {
|
|
if (this.isInitialized) {
|
|
console.warn('ROI Calculator already initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create the main calculator instance
|
|
this.calculator = new ROICalculator();
|
|
|
|
// Create UI and chart managers
|
|
this.uiManager = new UIManager(this.calculator);
|
|
this.chartManager = new ChartManager(this.calculator);
|
|
this.exportManager = new ExportManager(this.calculator, this.uiManager);
|
|
|
|
// Replace the methods in calculator with manager methods
|
|
this.calculator.updateSummaryMetrics = () => this.uiManager.updateSummaryMetrics();
|
|
this.calculator.updateCharts = () => this.chartManager.updateCharts();
|
|
this.calculator.updateComparisonTable = () => this.uiManager.updateComparisonTable();
|
|
this.calculator.updateMonthlyBreakdown = () => this.uiManager.updateMonthlyBreakdown();
|
|
this.calculator.initializeCharts = () => this.chartManager.initializeCharts();
|
|
this.calculator.formatCurrency = (amount) => this.uiManager.formatCurrency(amount);
|
|
this.calculator.formatCurrencyDetailed = (amount) => this.uiManager.formatCurrencyDetailed(amount);
|
|
this.calculator.formatPercentage = (value) => this.uiManager.formatPercentage(value);
|
|
|
|
// Re-initialize charts with the chart manager
|
|
this.chartManager.initializeCharts();
|
|
this.calculator.charts = this.chartManager.charts;
|
|
|
|
// Initialize tooltips
|
|
this.initializeTooltips();
|
|
|
|
// Check export libraries
|
|
this.checkExportLibraries();
|
|
|
|
// Run initial calculation
|
|
this.calculator.updateCalculations();
|
|
|
|
this.isInitialized = true;
|
|
console.log('ROI Calculator initialized successfully');
|
|
} catch (error) {
|
|
console.error('Failed to initialize ROI Calculator:', error);
|
|
}
|
|
}
|
|
|
|
// Initialize tooltips with Bootstrap (loaded directly via CDN)
|
|
initializeTooltips() {
|
|
// Wait for Bootstrap to be available
|
|
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
|
|
try {
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
});
|
|
} catch (error) {
|
|
console.warn('Failed to initialize Bootstrap tooltips:', error);
|
|
this.initializeNativeTooltips();
|
|
}
|
|
} else {
|
|
// Retry after a short delay for deferred scripts
|
|
setTimeout(() => this.initializeTooltips(), 100);
|
|
}
|
|
}
|
|
|
|
initializeNativeTooltips() {
|
|
try {
|
|
var tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
tooltipElements.forEach(function (element) {
|
|
// Ensure the title attribute is set for native tooltips
|
|
var tooltipContent = element.getAttribute('title');
|
|
if (!tooltipContent) {
|
|
// Get tooltip content from data-bs-original-title or title attribute
|
|
tooltipContent = element.getAttribute('data-bs-original-title');
|
|
if (tooltipContent) {
|
|
element.setAttribute('title', tooltipContent);
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error initializing native tooltips:', error);
|
|
}
|
|
}
|
|
|
|
// Check if export libraries are loaded
|
|
checkExportLibraries() {
|
|
try {
|
|
const pdfButton = document.querySelector('button[onclick="exportToPDF()"]');
|
|
|
|
if (typeof window.jspdf === 'undefined') {
|
|
if (pdfButton) {
|
|
pdfButton.disabled = true;
|
|
pdfButton.innerHTML = '<i class="bi bi-file-pdf"></i> Loading PDF...';
|
|
}
|
|
|
|
// Retry after a short delay
|
|
setTimeout(() => {
|
|
if (typeof window.jspdf !== 'undefined' && pdfButton) {
|
|
pdfButton.disabled = false;
|
|
pdfButton.innerHTML = '<i class="bi bi-file-pdf"></i> Export PDF Report';
|
|
}
|
|
}, 2000);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking export libraries:', error);
|
|
}
|
|
}
|
|
|
|
// Public API methods for global function calls
|
|
updateCalculations() {
|
|
if (this.calculator) {
|
|
this.calculator.updateCalculations();
|
|
this.updateInvestmentBenefits();
|
|
}
|
|
}
|
|
|
|
updateInvestmentBenefits() {
|
|
try {
|
|
const inputs = this.calculator.getInputValues();
|
|
const investmentAmount = inputs.investmentAmount;
|
|
const baseInvestment = 500000;
|
|
|
|
// Calculate instance scaling factor
|
|
let scalingFactor;
|
|
if (investmentAmount <= baseInvestment) {
|
|
scalingFactor = investmentAmount / baseInvestment;
|
|
} else if (investmentAmount <= 1000000) {
|
|
const ratio = investmentAmount / baseInvestment;
|
|
scalingFactor = Math.pow(ratio, 1.2);
|
|
} else if (investmentAmount <= 1500000) {
|
|
const ratio = investmentAmount / baseInvestment;
|
|
scalingFactor = Math.pow(ratio, 1.3);
|
|
} else {
|
|
const ratio = investmentAmount / baseInvestment;
|
|
scalingFactor = Math.pow(ratio, 1.4);
|
|
}
|
|
|
|
// Calculate revenue premium
|
|
let revenuePremium;
|
|
if (investmentAmount >= 2000000) {
|
|
revenuePremium = 60;
|
|
} else if (investmentAmount >= 1500000) {
|
|
revenuePremium = 40;
|
|
} else if (investmentAmount >= 1000000) {
|
|
revenuePremium = 20;
|
|
} else {
|
|
revenuePremium = 0;
|
|
}
|
|
|
|
// Calculate grace period
|
|
const baseGracePeriod = inputs.gracePeriod;
|
|
let gracePeriodBonus;
|
|
if (investmentAmount >= 1500000) {
|
|
gracePeriodBonus = 12;
|
|
} else if (investmentAmount >= 1000000) {
|
|
gracePeriodBonus = 6;
|
|
} else {
|
|
gracePeriodBonus = Math.floor((investmentAmount - baseInvestment) / 250000);
|
|
}
|
|
const totalGracePeriod = Math.max(0, baseGracePeriod + gracePeriodBonus);
|
|
|
|
// Calculate max performance bonus
|
|
let maxBonus;
|
|
if (investmentAmount >= 2000000) {
|
|
maxBonus = 35;
|
|
} else if (investmentAmount >= 1500000) {
|
|
maxBonus = 25;
|
|
} else if (investmentAmount >= 1000000) {
|
|
maxBonus = 20;
|
|
} else {
|
|
maxBonus = 15;
|
|
}
|
|
|
|
// Update UI elements
|
|
const instanceScaling = document.getElementById('instance-scaling');
|
|
if (instanceScaling) {
|
|
instanceScaling.textContent = scalingFactor.toFixed(1) + 'x';
|
|
instanceScaling.className = scalingFactor >= 2.0 ? 'benefit-value text-success fw-bold' :
|
|
scalingFactor >= 1.5 ? 'benefit-value text-primary fw-bold' : 'benefit-value text-secondary fw-bold';
|
|
}
|
|
|
|
const revenuePremiumEl = document.getElementById('revenue-premium');
|
|
if (revenuePremiumEl) {
|
|
revenuePremiumEl.textContent = '+' + revenuePremium + '%';
|
|
revenuePremiumEl.className = revenuePremium >= 40 ? 'benefit-value text-success fw-bold' :
|
|
revenuePremium >= 20 ? 'benefit-value text-warning fw-bold' : 'benefit-value text-secondary fw-bold';
|
|
}
|
|
|
|
const gracePeriodEl = document.getElementById('grace-period-display');
|
|
if (gracePeriodEl) {
|
|
gracePeriodEl.textContent = totalGracePeriod + ' months';
|
|
gracePeriodEl.className = totalGracePeriod >= 12 ? 'benefit-value text-success fw-bold' :
|
|
totalGracePeriod >= 9 ? 'benefit-value text-info fw-bold' : 'benefit-value text-secondary fw-bold';
|
|
}
|
|
|
|
const maxBonusEl = document.getElementById('max-bonus');
|
|
if (maxBonusEl) {
|
|
maxBonusEl.textContent = maxBonus + '%';
|
|
maxBonusEl.className = maxBonus >= 30 ? 'benefit-value text-success fw-bold' :
|
|
maxBonus >= 20 ? 'benefit-value text-warning fw-bold' : 'benefit-value text-secondary fw-bold';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating investment benefits:', error);
|
|
}
|
|
}
|
|
|
|
exportToPDF() {
|
|
if (this.exportManager) {
|
|
this.exportManager.exportToPDF();
|
|
}
|
|
}
|
|
|
|
exportToCSV() {
|
|
if (this.exportManager) {
|
|
this.exportManager.exportToCSV();
|
|
}
|
|
}
|
|
|
|
updateInvestmentAmount(value) {
|
|
try {
|
|
const input = document.getElementById('investment-amount');
|
|
if (input) {
|
|
input.setAttribute('data-value', value);
|
|
input.value = InputUtils.formatNumberWithCommas(value);
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating investment amount:', error);
|
|
}
|
|
}
|
|
|
|
updateRevenuePerInstance(value) {
|
|
try {
|
|
const element = document.getElementById('revenue-per-instance');
|
|
if (element) {
|
|
element.value = value;
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating revenue per instance:', error);
|
|
}
|
|
}
|
|
|
|
updateServalaShare(value) {
|
|
try {
|
|
const element = document.getElementById('servala-share');
|
|
if (element) {
|
|
element.value = value;
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating servala share:', error);
|
|
}
|
|
}
|
|
|
|
updateGracePeriod(value) {
|
|
try {
|
|
const element = document.getElementById('grace-period');
|
|
if (element) {
|
|
element.value = value;
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating grace period:', error);
|
|
}
|
|
}
|
|
|
|
updateLoanRate(value) {
|
|
try {
|
|
const element = document.getElementById('loan-interest-rate');
|
|
if (element) {
|
|
element.value = value;
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating loan rate:', error);
|
|
}
|
|
}
|
|
|
|
updateCoreServiceRevenue(value) {
|
|
try {
|
|
const element = document.getElementById('core-service-revenue');
|
|
if (element) {
|
|
element.value = value;
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating core service revenue:', error);
|
|
}
|
|
}
|
|
|
|
updateCurrency(value) {
|
|
try {
|
|
const currencyElement = document.getElementById('currency');
|
|
if (currencyElement) {
|
|
currencyElement.value = value;
|
|
}
|
|
|
|
// Update all currency-related UI labels
|
|
this.updateCurrencyLabels(value);
|
|
|
|
// Update calculations to reflect new currency formatting
|
|
this.updateCalculations();
|
|
} catch (error) {
|
|
console.error('Error updating currency:', error);
|
|
}
|
|
}
|
|
|
|
updateCurrencyLabels(currency) {
|
|
try {
|
|
// Update investment amount prefix
|
|
const investmentPrefix = document.getElementById('investment-currency-prefix');
|
|
if (investmentPrefix) {
|
|
investmentPrefix.textContent = currency;
|
|
}
|
|
|
|
// Update investment min/max labels
|
|
const investmentMinLabel = document.getElementById('investment-min-label');
|
|
if (investmentMinLabel) {
|
|
investmentMinLabel.textContent = `${currency} 100K`;
|
|
}
|
|
|
|
const investmentMaxLabel = document.getElementById('investment-max-label');
|
|
if (investmentMaxLabel) {
|
|
investmentMaxLabel.textContent = `${currency} 2M`;
|
|
}
|
|
|
|
// Update revenue per instance suffix
|
|
const revenueSuffix = document.getElementById('revenue-currency-suffix');
|
|
if (revenueSuffix) {
|
|
revenueSuffix.textContent = `${currency}/month`;
|
|
}
|
|
|
|
// Update core service revenue suffix (it's a direct span, not ID-based)
|
|
const coreRevenueInput = document.getElementById('core-service-revenue');
|
|
if (coreRevenueInput) {
|
|
const coreRevenueSpan = coreRevenueInput.parentElement.querySelector('.input-group-text');
|
|
if (coreRevenueSpan) {
|
|
coreRevenueSpan.textContent = `${currency}/month`;
|
|
}
|
|
}
|
|
|
|
// Update all other currency labels throughout the interface
|
|
const currencyLabels = document.querySelectorAll('.currency-label');
|
|
currencyLabels.forEach(label => {
|
|
label.textContent = currency;
|
|
});
|
|
|
|
// Update range slider labels with currency
|
|
const revenueLabel = document.querySelector('label[for="revenue-per-instance"]');
|
|
if (revenueLabel) {
|
|
revenueLabel.innerHTML = revenueLabel.innerHTML.replace(/(CHF|EUR)/, currency);
|
|
}
|
|
|
|
const coreRevenueLabel = document.querySelector('label[for="core-service-revenue"]');
|
|
if (coreRevenueLabel) {
|
|
coreRevenueLabel.innerHTML = coreRevenueLabel.innerHTML.replace(/(CHF|EUR)/, currency);
|
|
}
|
|
|
|
// Update investment amount field display if it has a value
|
|
const investmentInput = document.getElementById('investment-amount');
|
|
if (investmentInput && investmentInput.getAttribute('data-value')) {
|
|
const currentValue = investmentInput.getAttribute('data-value');
|
|
investmentInput.value = InputUtils.formatNumberWithCommas(currentValue, currency);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating currency labels:', error);
|
|
}
|
|
}
|
|
|
|
updateScenarioChurn(scenarioKey, churnRate) {
|
|
try {
|
|
if (this.calculator && this.calculator.scenarios[scenarioKey]) {
|
|
this.calculator.scenarios[scenarioKey].churnRate = parseFloat(churnRate) / 100;
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating scenario churn:', error);
|
|
}
|
|
}
|
|
|
|
updateScenarioPhase(scenarioKey, phaseIndex, newInstancesPerMonth) {
|
|
try {
|
|
if (this.calculator && this.calculator.scenarios[scenarioKey] && this.calculator.scenarios[scenarioKey].phases[phaseIndex]) {
|
|
this.calculator.scenarios[scenarioKey].phases[phaseIndex].newInstancesPerMonth = parseInt(newInstancesPerMonth);
|
|
this.updateCalculations();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating scenario phase:', error);
|
|
}
|
|
}
|
|
|
|
resetAdvancedParameters() {
|
|
if (!confirm('Reset all advanced parameters to default values?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (!this.calculator) return;
|
|
|
|
// Reset Conservative
|
|
this.calculator.scenarios.conservative.churnRate = 0.02;
|
|
this.calculator.scenarios.conservative.phases = [
|
|
{ months: 6, newInstancesPerMonth: 50 },
|
|
{ months: 6, newInstancesPerMonth: 75 },
|
|
{ months: 12, newInstancesPerMonth: 100 },
|
|
{ months: 12, newInstancesPerMonth: 150 }
|
|
];
|
|
|
|
// Reset Moderate
|
|
this.calculator.scenarios.moderate.churnRate = 0.03;
|
|
this.calculator.scenarios.moderate.phases = [
|
|
{ months: 6, newInstancesPerMonth: 100 },
|
|
{ months: 6, newInstancesPerMonth: 200 },
|
|
{ months: 12, newInstancesPerMonth: 300 },
|
|
{ months: 12, newInstancesPerMonth: 400 }
|
|
];
|
|
|
|
// Reset Aggressive
|
|
this.calculator.scenarios.aggressive.churnRate = 0.05;
|
|
this.calculator.scenarios.aggressive.phases = [
|
|
{ months: 6, newInstancesPerMonth: 200 },
|
|
{ months: 6, newInstancesPerMonth: 400 },
|
|
{ months: 12, newInstancesPerMonth: 600 },
|
|
{ months: 12, newInstancesPerMonth: 800 }
|
|
];
|
|
|
|
// Update UI inputs
|
|
const inputMappings = [
|
|
['conservative-churn', '2.0'],
|
|
['conservative-phase-0', '50'],
|
|
['conservative-phase-1', '75'],
|
|
['conservative-phase-2', '100'],
|
|
['conservative-phase-3', '150'],
|
|
['moderate-churn', '3.0'],
|
|
['moderate-phase-0', '100'],
|
|
['moderate-phase-1', '200'],
|
|
['moderate-phase-2', '300'],
|
|
['moderate-phase-3', '400'],
|
|
['aggressive-churn', '5.0'],
|
|
['aggressive-phase-0', '200'],
|
|
['aggressive-phase-1', '400'],
|
|
['aggressive-phase-2', '600'],
|
|
['aggressive-phase-3', '800']
|
|
];
|
|
|
|
inputMappings.forEach(([id, value]) => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.value = value;
|
|
}
|
|
});
|
|
|
|
this.updateCalculations();
|
|
} catch (error) {
|
|
console.error('Error resetting advanced parameters:', error);
|
|
}
|
|
}
|
|
|
|
toggleScenario(scenarioKey) {
|
|
try {
|
|
const checkbox = document.getElementById(scenarioKey + '-enabled');
|
|
if (!checkbox || !this.calculator) return;
|
|
|
|
const enabled = checkbox.checked;
|
|
this.calculator.scenarios[scenarioKey].enabled = enabled;
|
|
|
|
const card = document.getElementById(scenarioKey + '-card');
|
|
if (card) {
|
|
if (enabled) {
|
|
card.classList.add('active');
|
|
card.classList.remove('disabled');
|
|
} else {
|
|
card.classList.remove('active');
|
|
card.classList.add('disabled');
|
|
}
|
|
}
|
|
|
|
this.updateCalculations();
|
|
} catch (error) {
|
|
console.error('Error toggling scenario:', error);
|
|
}
|
|
}
|
|
|
|
toggleCollapsible(elementId) {
|
|
try {
|
|
const content = document.getElementById(elementId);
|
|
if (!content) return;
|
|
|
|
const header = content.previousElementSibling;
|
|
if (!header) return;
|
|
|
|
const chevron = header.querySelector('.bi-chevron-down, .bi-chevron-up');
|
|
|
|
if (content.classList.contains('show')) {
|
|
content.classList.remove('show');
|
|
if (chevron) {
|
|
chevron.classList.remove('bi-chevron-up');
|
|
chevron.classList.add('bi-chevron-down');
|
|
}
|
|
} else {
|
|
content.classList.add('show');
|
|
if (chevron) {
|
|
chevron.classList.remove('bi-chevron-down');
|
|
chevron.classList.add('bi-chevron-up');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error toggling collapsible:', error);
|
|
}
|
|
}
|
|
|
|
resetCalculator() {
|
|
if (!confirm('Are you sure you want to reset all parameters to default values?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Reset input values
|
|
const investmentInput = document.getElementById('investment-amount');
|
|
if (investmentInput) {
|
|
investmentInput.setAttribute('data-value', '500000');
|
|
investmentInput.value = '500,000';
|
|
}
|
|
|
|
const resetMappings = [
|
|
['investment-slider', 500000],
|
|
['timeframe', 3],
|
|
['loan-interest-rate', 5.0],
|
|
['loan-rate-slider', 5.0],
|
|
['revenue-per-instance', 50],
|
|
['revenue-slider', 50],
|
|
['servala-share', 25],
|
|
['share-slider', 25],
|
|
['grace-period', 6],
|
|
['grace-slider', 6]
|
|
];
|
|
|
|
resetMappings.forEach(([id, value]) => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.value = value;
|
|
}
|
|
});
|
|
|
|
// Both models are now calculated simultaneously
|
|
|
|
// Reset scenarios
|
|
['conservative', 'moderate', 'aggressive'].forEach(scenario => {
|
|
const checkbox = document.getElementById(scenario + '-enabled');
|
|
const card = document.getElementById(scenario + '-card');
|
|
|
|
if (checkbox) checkbox.checked = true;
|
|
if (this.calculator) this.calculator.scenarios[scenario].enabled = true;
|
|
if (card) {
|
|
card.classList.add('active');
|
|
card.classList.remove('disabled');
|
|
}
|
|
});
|
|
|
|
// Reset advanced parameters
|
|
this.resetAdvancedParameters();
|
|
|
|
// Both models are now calculated simultaneously, no toggle needed
|
|
|
|
// Recalculate
|
|
this.updateCalculations();
|
|
} catch (error) {
|
|
console.error('Error resetting calculator:', error);
|
|
}
|
|
}
|
|
|
|
// toggleInvestmentModel removed - both models are now calculated simultaneously
|
|
|
|
updateMonthlyBreakdownFilters() {
|
|
try {
|
|
if (this.uiManager) {
|
|
this.uiManager.updateMonthlyBreakdown();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating monthly breakdown filters:', error);
|
|
}
|
|
}
|
|
|
|
logout() {
|
|
if (!confirm('Are you sure you want to logout?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create a form to submit logout request
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = window.location.pathname;
|
|
|
|
// Add CSRF token from page meta tag or cookie
|
|
const csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrfmiddlewaretoken';
|
|
csrfInput.value = InputUtils.getCSRFToken();
|
|
form.appendChild(csrfInput);
|
|
|
|
// Add logout parameter
|
|
const logoutInput = document.createElement('input');
|
|
logoutInput.type = 'hidden';
|
|
logoutInput.name = 'logout';
|
|
logoutInput.value = 'true';
|
|
form.appendChild(logoutInput);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
} catch (error) {
|
|
console.error('Error during logout:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the application when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
window.ROICalculatorApp = new ROICalculatorApp();
|
|
window.ROICalculatorApp.initialize();
|
|
}); |