/** * Price Calculator for Service Offerings * Handles interactive pricing calculation with sliders and plan selection */ class PriceCalculator { constructor() { this.pricingData = null; this.storagePrice = null; this.currentOffering = null; this.selectedConfiguration = null; this.replicaInfo = 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(); } // Setup order button click handler this.setupOrderButton(); } // Initialize DOM element references initElements() { // Calculator controls this.cpuRange = document.getElementById('cpuRange'); this.memoryRange = document.getElementById('memoryRange'); this.storageRange = document.getElementById('storageRange'); this.instancesRange = document.getElementById('instancesRange'); this.cpuValue = document.getElementById('cpuValue'); this.memoryValue = document.getElementById('memoryValue'); this.storageValue = document.getElementById('storageValue'); this.instancesValue = document.getElementById('instancesValue'); 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.planInstances = document.getElementById('planInstances'); this.planServiceLevel = document.getElementById('planServiceLevel'); this.managedServicePrice = document.getElementById('managedServicePrice'); this.storagePriceEl = document.getElementById('storagePrice'); this.storageAmount = document.getElementById('storageAmount'); this.totalPrice = document.getElementById('totalPrice'); // Order button this.orderButton = document.querySelector('a[href="#order-form"]'); } // Update slider display values (min/max text below sliders) updateSliderDisplayValues() { // Update CPU slider display if (this.cpuRange) { const cpuMinDisplay = document.getElementById('cpuMinDisplay'); const cpuMaxDisplay = document.getElementById('cpuMaxDisplay'); if (cpuMinDisplay) cpuMinDisplay.textContent = this.cpuRange.min; if (cpuMaxDisplay) cpuMaxDisplay.textContent = this.cpuRange.max; } // Update Memory slider display if (this.memoryRange) { const memoryMinDisplay = document.getElementById('memoryMinDisplay'); const memoryMaxDisplay = document.getElementById('memoryMaxDisplay'); if (memoryMinDisplay) memoryMinDisplay.textContent = this.memoryRange.min + ' GB'; if (memoryMaxDisplay) memoryMaxDisplay.textContent = this.memoryRange.max + ' GB'; } // Update Storage slider display if (this.storageRange) { const storageMinDisplay = document.getElementById('storageMinDisplay'); const storageMaxDisplay = document.getElementById('storageMaxDisplay'); if (storageMinDisplay) storageMinDisplay.textContent = this.storageRange.min + ' GB'; if (storageMaxDisplay) storageMaxDisplay.textContent = this.storageRange.max + ' GB'; } // Update Instances slider display if (this.instancesRange) { const instancesMinDisplay = document.getElementById('instancesMinDisplay'); const instancesMaxDisplay = document.getElementById('instancesMaxDisplay'); if (instancesMinDisplay) instancesMinDisplay.textContent = this.instancesRange.min; if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.instancesRange.max; } } // Setup order button click handler setupOrderButton() { if (this.orderButton) { this.orderButton.addEventListener('click', (e) => { e.preventDefault(); this.handleOrderClick(); }); } } // Handle order button click handleOrderClick() { if (this.selectedConfiguration) { // Pre-fill the contact form with configuration details this.prefillContactForm(); // Scroll to the contact form const contactForm = document.getElementById('order-form'); if (contactForm) { contactForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } } // Pre-fill contact form with selected configuration prefillContactForm() { if (!this.selectedConfiguration) return; const config = this.selectedConfiguration; // Create configuration summary message const configMessage = this.generateConfigurationMessage(config); // Find and fill the message textarea in the contact form const messageField = document.querySelector('#order-form textarea[name="message"]'); if (messageField) { messageField.value = configMessage; } // Store configuration details in hidden field const detailsField = document.querySelector('#order-form input[name="details"]'); if (detailsField) { detailsField.value = JSON.stringify({ plan: config.planName, vcpus: config.vcpus, memory: config.memory, storage: config.storage, instances: config.instances, serviceLevel: config.serviceLevel, totalPrice: config.totalPrice }); } } // Generate human-readable configuration message generateConfigurationMessage(config) { return `I would like to order the following configuration: Plan: ${config.planName} (${config.planGroup}) vCPUs: ${config.vcpus} Memory: ${config.memory} GB Storage: ${config.storage} GB Instances: ${config.instances} Service Level: ${config.serviceLevel} Total Monthly Price: CHF ${config.totalPrice} Please contact me with next steps for ordering this configuration.`; } // 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(); // Extract storage price from the first available plan this.extractStoragePrice(); this.setupEventListeners(); this.populatePlanDropdown(); this.updatePricing(); } catch (error) { console.error('Error loading pricing data:', error); this.showError('Failed to load pricing information'); } } // Extract replica information and storage price from pricing data extractStoragePrice() { if (!this.pricingData) return; // Find the first plan with storage pricing data and replica info for (const groupName of Object.keys(this.pricingData)) { const group = this.pricingData[groupName]; for (const serviceLevel of Object.keys(group)) { const plans = group[serviceLevel]; if (plans.length > 0 && plans[0].storage_price !== undefined) { this.storagePrice = parseFloat(plans[0].storage_price); this.replicaInfo = { ha_replica_min: plans[0].ha_replica_min || 1, ha_replica_max: plans[0].ha_replica_max || 1 }; return; } } } } // Setup event listeners for calculator controls setupEventListeners() { if (!this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) 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(); }); this.instancesRange.addEventListener('input', () => { this.instancesValue.textContent = this.instancesRange.value; this.updatePricing(); }); // Service level change listeners this.serviceLevelInputs.forEach(input => { input.addEventListener('change', () => { this.updateInstancesSlider(); 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(); } }); } // Initialize instances slider this.updateInstancesSlider(); } // Update instances slider based on service level and replica info updateInstancesSlider() { if (!this.instancesRange || !this.replicaInfo) return; const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; if (serviceLevel === 'Guaranteed Availability') { // For GA, min is ha_replica_min this.instancesRange.min = this.replicaInfo.ha_replica_min; this.instancesRange.value = Math.max(this.instancesRange.value, this.replicaInfo.ha_replica_min); } else { // For BE, min is 1 this.instancesRange.min = 1; this.instancesRange.value = Math.max(this.instancesRange.value, 1); } // Set max to ha_replica_max this.instancesRange.max = this.replicaInfo.ha_replica_max; // Update display value this.instancesValue.textContent = this.instancesRange.value; // Update the min/max display under the slider using direct IDs const instancesMinDisplay = document.getElementById('instancesMinDisplay'); const instancesMaxDisplay = document.getElementById('instancesMaxDisplay'); if (instancesMinDisplay) instancesMinDisplay.textContent = this.instancesRange.min; if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.instancesRange.max; } // 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.updateInstancesSlider(); 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 will call updateSliderDisplayValues() 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); } if (maxMemory > 0) { this.memoryRange.max = Math.ceil(maxMemory); } // Update display values after changing min/max - moved to end and call explicitly this.updateSliderDisplayValues(); } // 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); const instances = parseInt(this.instancesRange?.value || 1); this.showPlanDetails(selectedPlan, storage, instances); this.updateStatusMessage('Plan selected directly!', 'success'); } // Main pricing update function updatePricing() { if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) 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 instances = parseInt(this.instancesRange.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, instances); 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, instances) { 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'; // Get current service level const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value || 'Best Effort'; // 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'; if (this.planInstances) this.planInstances.textContent = instances; if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel; // Calculate pricing using storage price from the plan data const computePriceValue = parseFloat(plan.compute_plan_price); const servicePriceValue = parseFloat(plan.sla_price); const managedServicePricePerInstance = computePriceValue + servicePriceValue; const managedServicePrice = managedServicePricePerInstance * instances; // Use storage price from plan data or fallback to instance variable const storageUnitPrice = plan.storage_price !== undefined ? parseFloat(plan.storage_price) : this.storagePrice; const storagePriceValue = storage * storageUnitPrice * instances; 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); // Store current configuration for order button this.selectedConfiguration = { planName: plan.compute_plan, planGroup: plan.groupName, vcpus: plan.vcpus, memory: plan.ram, storage: storage, instances: instances, serviceLevel: serviceLevel, totalPrice: 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(); } });