fix critical issues in ROI calculator
- Replace hardcoded Django template tags in JavaScript with runtime CSRF token retrieval - Add comprehensive error handling for Chart.js dependencies and missing DOM elements - Enhance input validation with safe fallbacks for malformed number parsing - Add graceful degradation when external libraries fail to load 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2981be12df
commit
51d80364c0
2 changed files with 240 additions and 90 deletions
|
|
@ -48,16 +48,58 @@ class ROICalculator {
|
|||
}
|
||||
|
||||
getInputValues() {
|
||||
const investmentModel = document.querySelector('input[name="investment-model"]:checked').value;
|
||||
try {
|
||||
// Get investment model with fallback
|
||||
const investmentModelElement = document.querySelector('input[name="investment-model"]:checked');
|
||||
const investmentModel = investmentModelElement ? investmentModelElement.value : 'direct';
|
||||
|
||||
// Get investment amount with validation
|
||||
const investmentAmountElement = document.getElementById('investment-amount');
|
||||
const investmentAmountValue = investmentAmountElement ? investmentAmountElement.getAttribute('data-value') : '500000';
|
||||
const investmentAmount = parseFloat(investmentAmountValue) || 500000;
|
||||
|
||||
// Get timeframe with validation
|
||||
const timeframeElement = document.getElementById('timeframe');
|
||||
const timeframe = timeframeElement ? parseInt(timeframeElement.value) || 3 : 3;
|
||||
|
||||
// Get loan interest rate with validation
|
||||
const loanRateElement = document.getElementById('loan-interest-rate');
|
||||
const loanInterestRate = loanRateElement ? (parseFloat(loanRateElement.value) || 5.0) / 100 : 0.05;
|
||||
|
||||
// Get revenue per instance with validation
|
||||
const revenueElement = document.getElementById('revenue-per-instance');
|
||||
const revenuePerInstance = revenueElement ? parseFloat(revenueElement.value) || 50 : 50;
|
||||
|
||||
// Get servala share with validation
|
||||
const shareElement = document.getElementById('servala-share');
|
||||
const servalaShare = shareElement ? (parseFloat(shareElement.value) || 25) / 100 : 0.25;
|
||||
|
||||
// Get grace period with validation
|
||||
const graceElement = document.getElementById('grace-period');
|
||||
const gracePeriod = graceElement ? parseInt(graceElement.value) || 6 : 6;
|
||||
|
||||
return {
|
||||
investmentAmount: parseFloat(document.getElementById('investment-amount').getAttribute('data-value')),
|
||||
timeframe: parseInt(document.getElementById('timeframe').value),
|
||||
investmentModel: investmentModel,
|
||||
loanInterestRate: parseFloat(document.getElementById('loan-interest-rate').value) / 100,
|
||||
revenuePerInstance: parseFloat(document.getElementById('revenue-per-instance').value),
|
||||
servalaShare: parseFloat(document.getElementById('servala-share').value) / 100,
|
||||
gracePeriod: parseInt(document.getElementById('grace-period').value)
|
||||
investmentAmount,
|
||||
timeframe,
|
||||
investmentModel,
|
||||
loanInterestRate,
|
||||
revenuePerInstance,
|
||||
servalaShare,
|
||||
gracePeriod
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting input values:', error);
|
||||
// Return safe default values
|
||||
return {
|
||||
investmentAmount: 500000,
|
||||
timeframe: 3,
|
||||
investmentModel: 'direct',
|
||||
loanInterestRate: 0.05,
|
||||
revenuePerInstance: 50,
|
||||
servalaShare: 0.25,
|
||||
gracePeriod: 6
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
calculateScenario(scenarioKey, inputs) {
|
||||
|
|
@ -242,8 +284,21 @@ class ROICalculator {
|
|||
}
|
||||
|
||||
initializeCharts() {
|
||||
// Check if Chart.js is available
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.error('Chart.js library not loaded. Charts will not be available.');
|
||||
this.showChartError('Chart.js library failed to load. Please refresh the page.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Instance Growth Chart
|
||||
const instanceCtx = document.getElementById('instanceGrowthChart').getContext('2d');
|
||||
const instanceCanvas = document.getElementById('instanceGrowthChart');
|
||||
if (!instanceCanvas) {
|
||||
console.error('Instance growth chart canvas not found');
|
||||
return;
|
||||
}
|
||||
const instanceCtx = instanceCanvas.getContext('2d');
|
||||
this.charts.instanceGrowth = new Chart(instanceCtx, {
|
||||
type: 'line',
|
||||
data: { labels: [], datasets: [] },
|
||||
|
|
@ -266,7 +321,12 @@ class ROICalculator {
|
|||
});
|
||||
|
||||
// Revenue Chart
|
||||
const revenueCtx = document.getElementById('revenueChart').getContext('2d');
|
||||
const revenueCanvas = document.getElementById('revenueChart');
|
||||
if (!revenueCanvas) {
|
||||
console.error('Revenue chart canvas not found');
|
||||
return;
|
||||
}
|
||||
const revenueCtx = revenueCanvas.getContext('2d');
|
||||
this.charts.revenue = new Chart(revenueCtx, {
|
||||
type: 'line',
|
||||
data: { labels: [], datasets: [] },
|
||||
|
|
@ -289,7 +349,12 @@ class ROICalculator {
|
|||
});
|
||||
|
||||
// Cash Flow Chart
|
||||
const cashFlowCtx = document.getElementById('cashFlowChart').getContext('2d');
|
||||
const cashFlowCanvas = document.getElementById('cashFlowChart');
|
||||
if (!cashFlowCanvas) {
|
||||
console.error('Cash flow chart canvas not found');
|
||||
return;
|
||||
}
|
||||
const cashFlowCtx = cashFlowCanvas.getContext('2d');
|
||||
this.charts.cashFlow = new Chart(cashFlowCtx, {
|
||||
type: 'bar',
|
||||
data: { labels: [], datasets: [] },
|
||||
|
|
@ -309,11 +374,33 @@ class ROICalculator {
|
|||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing charts:', error);
|
||||
this.showChartError('Failed to initialize charts. Please refresh the page.');
|
||||
}
|
||||
}
|
||||
|
||||
showChartError(message) {
|
||||
// Show error message in place of charts
|
||||
const chartContainers = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart'];
|
||||
chartContainers.forEach(containerId => {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
container.style.display = 'flex';
|
||||
container.style.justifyContent = 'center';
|
||||
container.style.alignItems = 'center';
|
||||
container.style.minHeight = '300px';
|
||||
container.style.backgroundColor = '#f8f9fa';
|
||||
container.style.border = '1px solid #dee2e6';
|
||||
container.style.borderRadius = '4px';
|
||||
container.innerHTML = `<div class="text-muted text-center"><i class="bi bi-exclamation-triangle"></i><br>${message}</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateCharts() {
|
||||
const scenarios = Object.keys(this.results);
|
||||
if (scenarios.length === 0) return;
|
||||
if (scenarios.length === 0 || !this.charts.instanceGrowth) return;
|
||||
|
||||
const colors = {
|
||||
conservative: '#28a745',
|
||||
|
|
@ -530,10 +617,32 @@ function formatNumberWithCommas(num) {
|
|||
}
|
||||
|
||||
function parseFormattedNumber(str) {
|
||||
return parseFloat(str.replace(/,/g, '')) || 0;
|
||||
if (typeof str !== 'string') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remove all non-numeric characters except decimal points and commas
|
||||
const cleaned = str.replace(/[^\d,.-]/g, '');
|
||||
|
||||
// Handle empty string
|
||||
if (!cleaned) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Remove commas and parse as float
|
||||
const result = parseFloat(cleaned.replace(/,/g, ''));
|
||||
|
||||
// Return 0 for invalid numbers or NaN
|
||||
return isNaN(result) ? 0 : result;
|
||||
}
|
||||
|
||||
function handleInvestmentAmountInput(input) {
|
||||
if (!input || typeof input.value !== 'string') {
|
||||
console.error('Invalid input element provided to handleInvestmentAmountInput');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove non-numeric characters except commas
|
||||
let value = input.value.replace(/[^\d,]/g, '');
|
||||
|
||||
|
|
@ -541,8 +650,14 @@ function handleInvestmentAmountInput(input) {
|
|||
let numericValue = parseFormattedNumber(value);
|
||||
|
||||
// Enforce min/max limits
|
||||
if (numericValue < 100000) numericValue = 100000;
|
||||
if (numericValue > 2000000) numericValue = 2000000;
|
||||
const minValue = 100000;
|
||||
const maxValue = 2000000;
|
||||
|
||||
if (numericValue < minValue) {
|
||||
numericValue = minValue;
|
||||
} else if (numericValue > maxValue) {
|
||||
numericValue = maxValue;
|
||||
}
|
||||
|
||||
// Update the data attribute with the raw numeric value
|
||||
input.setAttribute('data-value', numericValue.toString());
|
||||
|
|
@ -550,11 +665,20 @@ function handleInvestmentAmountInput(input) {
|
|||
// Format and display the value with commas
|
||||
input.value = formatNumberWithCommas(numericValue);
|
||||
|
||||
// Update the slider
|
||||
document.getElementById('investment-slider').value = numericValue;
|
||||
// Update the slider if it exists
|
||||
const slider = document.getElementById('investment-slider');
|
||||
if (slider) {
|
||||
slider.value = numericValue;
|
||||
}
|
||||
|
||||
// Trigger calculations
|
||||
updateCalculations();
|
||||
} catch (error) {
|
||||
console.error('Error handling investment amount input:', error);
|
||||
// Set a safe default value
|
||||
input.setAttribute('data-value', '500000');
|
||||
input.value = '500,000';
|
||||
}
|
||||
}
|
||||
|
||||
function updateInvestmentAmount(value) {
|
||||
|
|
@ -974,13 +1098,13 @@ function logout() {
|
|||
// Create a form to submit logout request
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{% url "services:csp_roi_calculator" %}';
|
||||
form.action = window.location.pathname;
|
||||
|
||||
// Add CSRF token
|
||||
// Add CSRF token from page meta tag or cookie
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = 'csrfmiddlewaretoken';
|
||||
csrfInput.value = '{{ csrf_token }}';
|
||||
csrfInput.value = getCSRFToken();
|
||||
form.appendChild(csrfInput);
|
||||
|
||||
// Add logout parameter
|
||||
|
|
@ -994,3 +1118,25 @@ function logout() {
|
|||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get CSRF token
|
||||
function getCSRFToken() {
|
||||
// Try to get CSRF token from meta tag first
|
||||
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
||||
if (metaTag) {
|
||||
return metaTag.getAttribute('content');
|
||||
}
|
||||
|
||||
// Fallback to cookie method
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [name, value] = cookie.trim().split('=');
|
||||
if (name === 'csrftoken') {
|
||||
return decodeURIComponent(value);
|
||||
}
|
||||
}
|
||||
|
||||
// If no CSRF token found, return empty string (will cause server error, but won't break JS)
|
||||
console.error('CSRF token not found');
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
|
||||
{% block title %}CSP ROI Calculator{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" type="text/css" href='{% static "css/roi-calculator.css" %}'>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue