diff --git a/hub/services/static/js/price-calculator.js b/hub/services/static/js/price-calculator.js index 96069fe..54b2af9 100644 --- a/hub/services/static/js/price-calculator.js +++ b/hub/services/static/js/price-calculator.js @@ -343,6 +343,880 @@ Please contact me with next steps for ordering this configuration.`; // Update pricing with the selected plan this.updatePricingWithPlan(selectedPlan); } else { + // Auto-select mode - reset sliders to default values + this.resetSlidersToDefaults(); + + // Auto-select mode - fade sliders back in + this.fadeInSliders(['cpu', 'memory']); + + // Auto-select mode - update addons and recalculate + this.updateAddons(); + 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.updateAddons(); + 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); + }); + } + + // Update addons based on current configuration + updateAddons() { + if (!this.addonsContainer || !this.addonsData) { + // Hide addons section if no container or data + const addonsSection = document.getElementById('addonsSection'); + if (addonsSection) addonsSection.style.display = 'none'; + return; + } + + const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; + if (!serviceLevel || !this.addonsData[serviceLevel]) { + // Hide addons section if no service level or no addons for this level + const addonsSection = document.getElementById('addonsSection'); + if (addonsSection) addonsSection.style.display = 'none'; + return; + } + + const addons = this.addonsData[serviceLevel]; + + // Clear existing addons + this.addonsContainer.innerHTML = ''; + + // Show or hide addons section based on availability + const addonsSection = document.getElementById('addonsSection'); + if (addons && addons.length > 0) { + if (addonsSection) addonsSection.style.display = 'block'; + } else { + if (addonsSection) addonsSection.style.display = 'none'; + return; + } + + // Add each addon + addons.forEach(addon => { + const addonElement = document.createElement('div'); + addonElement.className = `addon-item mb-2 p-2 border rounded ${addon.is_mandatory ? 'bg-light' : ''}`; + + addonElement.innerHTML = ` +
+ + +
+ `; + + this.addonsContainer.appendChild(addonElement); + + // Add event listener for optional addons + if (!addon.is_mandatory) { + const checkbox = addonElement.querySelector('.addon-checkbox'); + checkbox.addEventListener('change', () => { + // Update addon prices and recalculate total + this.updateAddonPrices(); + this.updatePricing(); + }); + } + }); + + // Update addon prices + this.updateAddonPrices(); + } // Update addon prices based on current configuration + updateAddonPrices() { + if (!this.addonsContainer) return; + + const cpus = parseInt(this.cpuRange?.value || 2); + const memory = parseInt(this.memoryRange?.value || 4); + const storage = parseInt(this.storageRange?.value || 20); + const instances = parseInt(this.instancesRange?.value || 1); + + // Find the current plan data to get variable_unit for addon calculations + const matchedPlan = this.getCurrentPlan(); + const variableUnit = matchedPlan?.variable_unit || 'CPU'; + const units = variableUnit === 'CPU' ? cpus : memory; + const totalUnits = units * instances; + + const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox'); + addonCheckboxes.forEach(checkbox => { + const addon = JSON.parse(checkbox.dataset.addon); + const priceElement = checkbox.parentElement.querySelector('.addon-price-value'); + + let calculatedPrice = 0; + + // Calculate addon price based on type + if (addon.addon_type === 'BASE_FEE') { + // Base fee: price per instance + calculatedPrice = parseFloat(addon.price || 0) * instances; + } else if (addon.addon_type === 'UNIT_RATE') { + // Unit rate: price per unit (CPU or memory) across all instances + calculatedPrice = parseFloat(addon.price_per_unit || 0) * totalUnits; + } + + // Update the display price + if (priceElement) { + priceElement.textContent = calculatedPrice.toFixed(2); + } + + // Store the calculated price for later use in total calculations + checkbox.dataset.calculatedPrice = calculatedPrice.toString(); + }); + } + + // Get current plan based on configuration + getCurrentPlan() { + const cpus = parseInt(this.cpuRange?.value || 2); + const memory = parseInt(this.memoryRange?.value || 4); + const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; + + if (this.planSelect?.value) { + return JSON.parse(this.planSelect.value); + } + + return this.findBestMatchingPlan(cpus, memory, serviceLevel); + } + + // 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); + + // Update addon prices first to ensure calculated prices are current + this.updateAddonPrices(); + + 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; + + // Update addon prices first to ensure they're current + this.updateAddonPrices(); + + // 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); + const storage = parseInt(this.storageRange.value); + const instances = parseInt(this.instancesRange.value); + + // Update addon prices for current configuration + this.updateAddonPrices(); + this.showPlanDetails(selectedPlan, storage, instances); + this.updateStatusMessage('Plan selected directly!', 'success'); + } + } + + // 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; + + // Ensure addon prices are calculated with current configuration + this.updateAddonPrices(); + + // Calculate pricing using final price from plan data (which already includes mandatory addons) + // plan.final_price = compute_plan_price + sla_price (where sla_price includes mandatory addons) + const managedServicePricePerInstance = parseFloat(plan.final_price); + + // Collect addon information for display and calculation + let mandatoryAddonTotal = 0; + let optionalAddonTotal = 0; + const mandatoryAddons = []; + const selectedOptionalAddons = []; + + if (this.addonsContainer) { + const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox'); + addonCheckboxes.forEach(checkbox => { + const addon = JSON.parse(checkbox.dataset.addon); + const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0); + + if (addon.is_mandatory) { + // Mandatory addons are already included in plan.final_price + // We collect them for display purposes only + mandatoryAddons.push({ + name: addon.name, + price: calculatedPrice.toFixed(2) + }); + } else if (checkbox.checked) { + // Only count checked optional addons + optionalAddonTotal += calculatedPrice; + selectedOptionalAddons.push({ + name: addon.name, + price: calculatedPrice.toFixed(2) + }); + } + }); + } + + 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; + + // Total price = managed service price (includes mandatory addons) + storage + optional addons + const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal; + + // 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); + + // Update addon pricing display + this.updateAddonPricingDisplay(mandatoryAddons, selectedOptionalAddons); + + // 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), + addons: [...mandatoryAddons, ...selectedOptionalAddons] + }; + } + + // Update addon pricing display in the results panel + updateAddonPricingDisplay(mandatoryAddons, selectedOptionalAddons) { + if (!this.addonPricingContainer) return; + + // Clear existing addon pricing display + this.addonPricingContainer.innerHTML = ''; + + // Add mandatory addons to pricing breakdown (for informational purposes only) + if (mandatoryAddons && mandatoryAddons.length > 0) { + // Add a note explaining mandatory addons are included + const mandatoryNote = document.createElement('div'); + mandatoryNote.className = 'text-muted small mb-2'; + mandatoryNote.innerHTML = 'Required add-ons (included in managed service price):'; + this.addonPricingContainer.appendChild(mandatoryNote); + + mandatoryAddons.forEach(addon => { + const addonRow = document.createElement('div'); + addonRow.className = 'd-flex justify-content-between mb-1 ps-3'; + addonRow.innerHTML = ` + ${addon.name} + CHF ${addon.price} + `; + this.addonPricingContainer.appendChild(addonRow); + }); + + // Add separator if there are also optional addons + if (selectedOptionalAddons && selectedOptionalAddons.length > 0) { + const separator = document.createElement('hr'); + separator.className = 'my-2'; + this.addonPricingContainer.appendChild(separator); + } + } + + // Add optional addons to pricing breakdown (these are added to total) + if (selectedOptionalAddons && selectedOptionalAddons.length > 0) { + selectedOptionalAddons.forEach(addon => { + const addonRow = document.createElement('div'); + addonRow.className = 'd-flex justify-content-between mb-2'; + addonRow.innerHTML = ` + Add-on: ${addon.name} + CHF ${addon.price} + `; + this.addonPricingContainer.appendChild(addonRow); + }); + } + } + + // 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'; + } + } + + // Fade out specified sliders when plan is manually selected + fadeOutSliders(sliderTypes) { + sliderTypes.forEach(type => { + const sliderContainer = this.getSliderContainer(type); + if (sliderContainer) { + sliderContainer.style.transition = 'opacity 0.3s ease-in-out'; + sliderContainer.style.opacity = '0.3'; + sliderContainer.style.pointerEvents = 'none'; + + // Add visual indicator that sliders are disabled + const slider = sliderContainer.querySelector('.form-range'); + if (slider) { + slider.style.cursor = 'not-allowed'; + } + } + }); + } + + // Fade in specified sliders when auto-select mode is chosen + fadeInSliders(sliderTypes) { + sliderTypes.forEach(type => { + const sliderContainer = this.getSliderContainer(type); + if (sliderContainer) { + sliderContainer.style.transition = 'opacity 0.3s ease-in-out'; + sliderContainer.style.opacity = '1'; + sliderContainer.style.pointerEvents = 'auto'; + + // Remove visual indicator + const slider = sliderContainer.querySelector('.form-range'); + if (slider) { + slider.style.cursor = 'pointer'; + } + } + }); + } + + // Get slider container element by type + getSliderContainer(type) { + switch (type) { + case 'cpu': + return this.cpuRange?.closest('.mb-4'); + case 'memory': + return this.memoryRange?.closest('.mb-4'); + case 'storage': + return this.storageRange?.closest('.mb-4'); + case 'instances': + return this.instancesRange?.closest('.mb-4'); + default: + return null; + } + } + + // Reset sliders to their default values + resetSlidersToDefaults() { + // Reset CPU slider to default value (2) + if (this.cpuRange) { + this.cpuRange.value = '2'; + if (this.cpuValue) this.cpuValue.textContent = '2'; + } + + // Reset Memory slider to default value (4 GB) + if (this.memoryRange) { + this.memoryRange.value = '4'; + if (this.memoryValue) this.memoryValue.textContent = '4'; + } + + // Reset Storage slider to default value (20 GB) + if (this.storageRange) { + this.storageRange.value = '20'; + if (this.storageValue) this.storageValue.textContent = '20'; + } + + // Reset Instances slider to default value (1) + if (this.instancesRange) { + this.instancesRange.value = '1'; + if (this.instancesValue) this.instancesValue.textContent = '1'; + } + } + + // 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, + addons: config.addons || [] + }); + } + } + + // Generate human-readable configuration message + generateConfigurationMessage(config) { + let message = `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}`; + + // Add addons to the message if any are selected + if (config.addons && config.addons.length > 0) { + message += '\n\nSelected Add-ons:'; + config.addons.forEach(addon => { + message += `\n- ${addon.name}: CHF ${addon.price}`; + }); + } + + message += `\n\nTotal Monthly Price: CHF ${config.totalPrice} + +Please contact me with next steps for ordering this configuration.`; + + return message; + } + + // 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'); + } + + const data = await response.json(); + this.pricingData = data.pricing || data; + + // Extract addons data from the plans - addons are embedded in each plan + this.extractAddonsData(); + + // Extract storage price from the first available plan + this.extractStoragePrice(); + + this.setupEventListeners(); + this.populatePlanDropdown(); + this.updateAddons(); + 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; + } + } + } + } + + // Extract addons data from pricing plans + extractAddonsData() { + if (!this.pricingData) return; + + this.addonsData = {}; + + // Extract addons from the first available plan for each service level + Object.keys(this.pricingData).forEach(groupName => { + const group = this.pricingData[groupName]; + Object.keys(group).forEach(serviceLevel => { + const plans = group[serviceLevel]; + if (plans.length > 0) { + // Use the first plan's addon data for this service level + const plan = plans[0]; + const allAddons = []; + + // Add mandatory addons + if (plan.mandatory_addons) { + plan.mandatory_addons.forEach(addon => { + allAddons.push({ + ...addon, + is_mandatory: true, + addon_type: addon.addon_type === "Base Fee" ? "BASE_FEE" : "UNIT_RATE" + }); + }); + } + + // Add optional addons + if (plan.optional_addons) { + plan.optional_addons.forEach(addon => { + allAddons.push({ + ...addon, + is_mandatory: false, + addon_type: addon.addon_type === "Base Fee" ? "BASE_FEE" : "UNIT_RATE" + }); + }); + } + + this.addonsData[serviceLevel] = allAddons; + } + }); + }); + } + + // 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.updateAddons(); + 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; + + // Fade out CPU and Memory sliders since plan is manually selected + this.fadeOutSliders(['cpu', 'memory']); + + // Update addons for the new configuration + this.updateAddons(); + // Update pricing with the selected plan + this.updatePricingWithPlan(selectedPlan); + } else { + // Auto-select mode - reset sliders to default values + this.resetSlidersToDefaults(); + // Auto-select mode - fade sliders back in this.fadeInSliders(['cpu', 'memory']);