diff --git a/hub/services/static/js/roi-calculator.js b/hub/services/static/js/roi-calculator.js index 4a3b36f..eba1b73 100644 --- a/hub/services/static/js/roi-calculator.js +++ b/hub/services/static/js/roi-calculator.js @@ -48,16 +48,58 @@ class ROICalculator { } getInputValues() { - const investmentModel = document.querySelector('input[name="investment-model"]:checked').value; - 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) - }; + 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, + 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,78 +284,123 @@ class ROICalculator { } initializeCharts() { - // Instance Growth Chart - const instanceCtx = document.getElementById('instanceGrowthChart').getContext('2d'); - this.charts.instanceGrowth = new Chart(instanceCtx, { - type: 'line', - data: { labels: [], datasets: [] }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { position: 'top' } - }, - scales: { - y: { - beginAtZero: true, - title: { display: true, text: 'Total Instances' } - }, - x: { - title: { display: true, text: 'Month' } - } - } - } - }); + // 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; + } - // Revenue Chart - const revenueCtx = document.getElementById('revenueChart').getContext('2d'); - this.charts.revenue = new Chart(revenueCtx, { - type: 'line', - data: { labels: [], datasets: [] }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { position: 'top' } - }, - scales: { - y: { - beginAtZero: true, - title: { display: true, text: 'Cumulative Revenue (CHF)' } - }, - x: { - title: { display: true, text: 'Month' } - } - } + try { + // Instance Growth Chart + const instanceCanvas = document.getElementById('instanceGrowthChart'); + if (!instanceCanvas) { + console.error('Instance growth chart canvas not found'); + return; } - }); - - // Cash Flow Chart - const cashFlowCtx = document.getElementById('cashFlowChart').getContext('2d'); - this.charts.cashFlow = new Chart(cashFlowCtx, { - type: 'bar', - data: { labels: [], datasets: [] }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { position: 'top' } - }, - scales: { - y: { - title: { display: true, text: 'Monthly Cash Flow (CHF)' } + const instanceCtx = instanceCanvas.getContext('2d'); + this.charts.instanceGrowth = new Chart(instanceCtx, { + type: 'line', + data: { labels: [], datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' } }, - x: { - title: { display: true, text: 'Month' } + scales: { + y: { + beginAtZero: true, + title: { display: true, text: 'Total Instances' } + }, + x: { + title: { display: true, text: 'Month' } + } } } + }); + + // Revenue Chart + 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: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' } + }, + scales: { + y: { + beginAtZero: true, + title: { display: true, text: 'Cumulative Revenue (CHF)' } + }, + x: { + title: { display: true, text: 'Month' } + } + } + } + }); + + // Cash Flow Chart + 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: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' } + }, + scales: { + y: { + title: { display: true, text: 'Monthly Cash Flow (CHF)' } + }, + x: { + title: { display: true, text: 'Month' } + } + } + } + }); + } 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 = `

${message}
`; } }); } 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,31 +617,68 @@ 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) { - // Remove non-numeric characters except commas - let value = input.value.replace(/[^\d,]/g, ''); + if (!input || typeof input.value !== 'string') { + console.error('Invalid input element provided to handleInvestmentAmountInput'); + return; + } - // Parse the numeric value - let numericValue = parseFormattedNumber(value); + try { + // Remove non-numeric characters except commas + let value = input.value.replace(/[^\d,]/g, ''); - // Enforce min/max limits - if (numericValue < 100000) numericValue = 100000; - if (numericValue > 2000000) numericValue = 2000000; + // Parse the numeric value + let numericValue = parseFormattedNumber(value); - // Update the data attribute with the raw numeric value - input.setAttribute('data-value', numericValue.toString()); + // Enforce min/max limits + const minValue = 100000; + const maxValue = 2000000; + + if (numericValue < minValue) { + numericValue = minValue; + } else if (numericValue > maxValue) { + numericValue = maxValue; + } - // Format and display the value with commas - input.value = formatNumberWithCommas(numericValue); + // Update the data attribute with the raw numeric value + input.setAttribute('data-value', numericValue.toString()); - // Update the slider - document.getElementById('investment-slider').value = numericValue; + // Format and display the value with commas + input.value = formatNumberWithCommas(numericValue); - // Trigger calculations - updateCalculations(); + // 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 ''; +} diff --git a/hub/services/templates/calculator/csp_roi_calculator.html b/hub/services/templates/calculator/csp_roi_calculator.html index c2a7975..462d584 100644 --- a/hub/services/templates/calculator/csp_roi_calculator.html +++ b/hub/services/templates/calculator/csp_roi_calculator.html @@ -3,6 +3,10 @@ {% block title %}CSP ROI Calculator{% endblock %} +{% block extra_head %} + +{% endblock %} + {% block extra_css %} {% endblock %}