/** * Price Calculator for Service Offerings * Handles interactive pricing calculation with sliders and plan selection */ class PriceCalculator { constructor() { this.pricingData = null; this.storagePrice = 0.15; // CHF per GB per month this.currentOffering = null; this.init(); } // Initialize calculator elements and event listeners init() { // Get offering info from URL const pathParts = window.location.pathname.split('/'); if (pathParts.length >= 4 && pathParts[1] === 'offering') { this.currentOffering = { provider_slug: pathParts[2], service_slug: pathParts[3] }; } // Initialize DOM elements this.initElements(); // Load pricing data and setup calculator if (this.currentOffering) { this.loadPricingData(); } } // Initialize DOM element references initElements() { // Calculator controls this.cpuRange = document.getElementById('cpuRange'); this.memoryRange = document.getElementById('memoryRange'); this.storageRange = document.getElementById('storageRange'); this.cpuValue = document.getElementById('cpuValue'); this.memoryValue = document.getElementById('memoryValue'); this.storageValue = document.getElementById('storageValue'); this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]'); this.planSelect = document.getElementById('planSelect'); // Result display elements this.planMatchStatus = document.getElementById('planMatchStatus'); this.selectedPlanDetails = document.getElementById('selectedPlanDetails'); this.noMatchFound = document.getElementById('noMatchFound'); // Plan detail elements this.planGroup = document.getElementById('planGroup'); this.planName = document.getElementById('planName'); this.planDescription = document.getElementById('planDescription'); this.planCpus = document.getElementById('planCpus'); this.planMemory = document.getElementById('planMemory'); this.managedServicePrice = document.getElementById('managedServicePrice'); this.storagePriceEl = document.getElementById('storagePrice'); this.storageAmount = document.getElementById('storageAmount'); this.totalPrice = document.getElementById('totalPrice'); } // Load pricing data from API endpoint async loadPricingData() { try { const response = await fetch(`/offering/${this.currentOffering.provider_slug}/${this.currentOffering.service_slug}/?pricing=json`); if (!response.ok) { throw new Error('Failed to load pricing data'); } this.pricingData = await response.json(); this.setupEventListeners(); this.populatePlanDropdown(); this.updatePricing(); } catch (error) { console.error('Error loading pricing data:', error); this.showError('Failed to load pricing information'); } } // Setup event listeners for calculator controls setupEventListeners() { if (!this.cpuRange || !this.memoryRange || !this.storageRange) return; // Setup service levels based on available data this.setupServiceLevels(); // Slider event listeners this.cpuRange.addEventListener('input', () => { this.cpuValue.textContent = this.cpuRange.value; this.updatePricing(); }); this.memoryRange.addEventListener('input', () => { this.memoryValue.textContent = this.memoryRange.value; this.updatePricing(); }); this.storageRange.addEventListener('input', () => { this.storageValue.textContent = this.storageRange.value; this.updatePricing(); }); // Service level change listeners this.serviceLevelInputs.forEach(input => { input.addEventListener('change', () => { this.populatePlanDropdown(); this.updatePricing(); }); }); // Plan selection listener if (this.planSelect) { this.planSelect.addEventListener('change', () => { if (this.planSelect.value) { const selectedPlan = JSON.parse(this.planSelect.value); // Update sliders to match selected plan this.cpuRange.value = selectedPlan.vcpus; this.memoryRange.value = selectedPlan.ram; this.cpuValue.textContent = selectedPlan.vcpus; this.memoryValue.textContent = selectedPlan.ram; this.updatePricingWithPlan(selectedPlan); } else { this.updatePricing(); } }); } } // Setup service levels dynamically from pricing data setupServiceLevels() { if (!this.pricingData) return; const serviceLevelGroup = document.getElementById('serviceLevelGroup'); if (!serviceLevelGroup) return; // Get all available service levels from the pricing data const availableServiceLevels = new Set(); Object.keys(this.pricingData).forEach(groupName => { const group = this.pricingData[groupName]; Object.keys(group).forEach(serviceLevel => { availableServiceLevels.add(serviceLevel); }); }); // Clear existing service level buttons serviceLevelGroup.innerHTML = ''; // Create buttons for each available service level let isFirst = true; availableServiceLevels.forEach(serviceLevel => { const inputId = `serviceLevel${serviceLevel.replace(/\s+/g, '')}`; // Create radio input const input = document.createElement('input'); input.type = 'radio'; input.className = 'btn-check'; input.name = 'serviceLevel'; input.id = inputId; input.value = serviceLevel; if (isFirst) { input.checked = true; isFirst = false; } // Create label const label = document.createElement('label'); label.className = 'btn btn-outline-primary'; label.setAttribute('for', inputId); label.textContent = serviceLevel; // Add event listener input.addEventListener('change', () => { this.populatePlanDropdown(); this.updatePricing(); }); serviceLevelGroup.appendChild(input); serviceLevelGroup.appendChild(label); }); // Update the serviceLevelInputs reference this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]'); // Calculate and set slider maximums based on available plans this.updateSliderMaximums(); } // Calculate maximum values for sliders based on available plans updateSliderMaximums() { if (!this.pricingData || !this.cpuRange || !this.memoryRange) return; let maxCpus = 0; let maxMemory = 0; // Find maximum CPU and memory across all plans Object.keys(this.pricingData).forEach(groupName => { const group = this.pricingData[groupName]; Object.keys(group).forEach(serviceLevel => { group[serviceLevel].forEach(plan => { const planCpus = parseFloat(plan.vcpus); const planMemory = parseFloat(plan.ram); if (planCpus > maxCpus) maxCpus = planCpus; if (planMemory > maxMemory) maxMemory = planMemory; }); }); }); // Set slider maximums with some padding if (maxCpus > 0) { this.cpuRange.max = Math.ceil(maxCpus); // Update the max display under the slider const cpuMaxDisplay = this.cpuRange.parentElement.querySelector('.d-flex.justify-content-between .text-muted span:last-child'); if (cpuMaxDisplay) cpuMaxDisplay.textContent = Math.ceil(maxCpus); } if (maxMemory > 0) { this.memoryRange.max = Math.ceil(maxMemory); // Update the max display under the slider const memoryMaxDisplay = this.memoryRange.parentElement.querySelector('.d-flex.justify-content-between .text-muted span:last-child'); if (memoryMaxDisplay) memoryMaxDisplay.textContent = Math.ceil(maxMemory) + ' GB'; } } // Populate plan dropdown based on selected service level populatePlanDropdown() { if (!this.planSelect || !this.pricingData) return; const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; if (!serviceLevel) return; // Clear existing options this.planSelect.innerHTML = ''; // Collect all plans for selected service level const availablePlans = []; Object.keys(this.pricingData).forEach(groupName => { const group = this.pricingData[groupName]; if (group[serviceLevel]) { group[serviceLevel].forEach(plan => { availablePlans.push({ ...plan, groupName: groupName }); }); } }); // Sort plans by vCPU, then by RAM availablePlans.sort((a, b) => { if (parseInt(a.vcpus) !== parseInt(b.vcpus)) { return parseInt(a.vcpus) - parseInt(b.vcpus); } return parseInt(a.ram) - parseInt(b.ram); }); // Add plans to dropdown availablePlans.forEach(plan => { const option = document.createElement('option'); option.value = JSON.stringify(plan); option.textContent = `${plan.compute_plan} - ${plan.vcpus} vCPUs, ${plan.ram} GB RAM`; this.planSelect.appendChild(option); }); } // Find best matching plan based on requirements findBestMatchingPlan(cpus, memory, serviceLevel) { if (!this.pricingData) return null; let bestMatch = null; let bestScore = Infinity; // Iterate through all groups and service levels Object.keys(this.pricingData).forEach(groupName => { const group = this.pricingData[groupName]; if (group[serviceLevel]) { group[serviceLevel].forEach(plan => { const planCpus = parseInt(plan.vcpus); const planMemory = parseInt(plan.ram); // Check if plan meets minimum requirements if (planCpus >= cpus && planMemory >= memory) { // Calculate efficiency score (lower is better) const cpuOverhead = planCpus - cpus; const memoryOverhead = planMemory - memory; const score = cpuOverhead + memoryOverhead + plan.final_price * 0.1; if (score < bestScore) { bestScore = score; bestMatch = { ...plan, groupName: groupName }; } } }); } }); return bestMatch; } // Update pricing with specific plan updatePricingWithPlan(selectedPlan) { const storage = parseInt(this.storageRange?.value || 20); this.showPlanDetails(selectedPlan, storage); this.updateStatusMessage('Plan selected directly!', 'success'); } // Main pricing update function updatePricing() { if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange) return; // Reset plan selection if in auto-select mode if (!this.planSelect?.value) { const cpus = parseInt(this.cpuRange.value); const memory = parseInt(this.memoryRange.value); const storage = parseInt(this.storageRange.value); const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; if (!serviceLevel) return; // Find best matching plan const matchedPlan = this.findBestMatchingPlan(cpus, memory, serviceLevel); if (matchedPlan) { this.showPlanDetails(matchedPlan, storage); this.updateStatusMessage('Perfect match found!', 'success'); } else { this.showNoMatch(); } } else { // Plan is directly selected, update storage pricing const selectedPlan = JSON.parse(this.planSelect.value); this.updatePricingWithPlan(selectedPlan); } } // Show plan details in the UI showPlanDetails(plan, storage) { if (!this.selectedPlanDetails) return; // Show plan details section this.planMatchStatus.style.display = 'block'; this.selectedPlanDetails.style.display = 'block'; if (this.noMatchFound) this.noMatchFound.style.display = 'none'; // Update plan information if (this.planGroup) this.planGroup.textContent = plan.groupName; if (this.planName) this.planName.textContent = plan.compute_plan; if (this.planDescription) this.planDescription.textContent = plan.compute_plan_group_description || ''; if (this.planCpus) this.planCpus.textContent = plan.vcpus; if (this.planMemory) this.planMemory.textContent = plan.ram + ' GB'; // Calculate pricing const computePriceValue = parseFloat(plan.compute_plan_price); const servicePriceValue = parseFloat(plan.sla_price); const managedServicePrice = computePriceValue + servicePriceValue; const storagePriceValue = storage * this.storagePrice; const totalPriceValue = managedServicePrice + storagePriceValue; // Update pricing display if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2); if (this.storagePriceEl) this.storagePriceEl.textContent = storagePriceValue.toFixed(2); if (this.storageAmount) this.storageAmount.textContent = storage; if (this.totalPrice) this.totalPrice.textContent = totalPriceValue.toFixed(2); } // Show no matching plan found showNoMatch() { if (this.planMatchStatus) this.planMatchStatus.style.display = 'none'; if (this.selectedPlanDetails) this.selectedPlanDetails.style.display = 'none'; if (this.noMatchFound) this.noMatchFound.style.display = 'block'; } // Update status message updateStatusMessage(message, type) { if (!this.planMatchStatus) return; const iconClass = type === 'success' ? 'bi-check-circle' : 'bi-info-circle'; const textClass = type === 'success' ? 'text-success' : ''; const alertClass = type === 'success' ? 'alert-success' : 'alert-info'; this.planMatchStatus.innerHTML = `${message}`; this.planMatchStatus.className = `alert ${alertClass} mb-3`; this.planMatchStatus.style.display = 'block'; } // Show error message showError(message) { if (this.planMatchStatus) { this.planMatchStatus.innerHTML = `${message}`; this.planMatchStatus.className = 'alert alert-danger mb-3'; this.planMatchStatus.style.display = 'block'; } } } // Initialize calculator when DOM is loaded document.addEventListener('DOMContentLoaded', () => { // Only initialize if we're on an offering detail page with pricing calculator if (document.getElementById('cpuRange')) { new PriceCalculator(); } });