467 lines
No EOL
17 KiB
JavaScript
467 lines
No EOL
17 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();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
});
|
|
|
|
// Check direct model radio button
|
|
const directModel = document.getElementById('direct-model');
|
|
if (directModel) {
|
|
directModel.checked = true;
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Reset investment model toggle
|
|
this.toggleInvestmentModel();
|
|
|
|
// Recalculate
|
|
this.updateCalculations();
|
|
} catch (error) {
|
|
console.error('Error resetting calculator:', error);
|
|
}
|
|
}
|
|
|
|
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: Guaranteed returns with fixed monthly payments';
|
|
} else {
|
|
if (loanSection) loanSection.style.display = 'none';
|
|
if (modelDescription) modelDescription.textContent = 'Direct Investment: Higher potential returns based on your sales performance';
|
|
}
|
|
|
|
this.updateCalculations();
|
|
} catch (error) {
|
|
console.error('Error toggling investment model:', 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();
|
|
}); |