From 67e1b4cab122ee3dc99cc37c5dcdec2572257b3e Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 15 Jul 2025 17:31:14 +0200 Subject: [PATCH] refactor price calculator into multiple files --- hub/services/static/js/price-calculator.js | 1937 +---------------- .../js/price-calculator/addon-manager.js | 176 ++ .../static/js/price-calculator/dom-manager.js | 160 ++ .../js/price-calculator/order-manager.js | 113 + .../js/price-calculator/plan-manager.js | 104 + .../js/price-calculator/price-calculator.js | 252 +++ .../price-calculator/pricing-data-manager.js | 190 ++ .../static/js/price-calculator/ui-manager.js | 269 +++ 8 files changed, 1324 insertions(+), 1877 deletions(-) create mode 100644 hub/services/static/js/price-calculator/addon-manager.js create mode 100644 hub/services/static/js/price-calculator/dom-manager.js create mode 100644 hub/services/static/js/price-calculator/order-manager.js create mode 100644 hub/services/static/js/price-calculator/plan-manager.js create mode 100644 hub/services/static/js/price-calculator/price-calculator.js create mode 100644 hub/services/static/js/price-calculator/pricing-data-manager.js create mode 100644 hub/services/static/js/price-calculator/ui-manager.js diff --git a/hub/services/static/js/price-calculator.js b/hub/services/static/js/price-calculator.js index 1fa9e2b..c2c8eb1 100644 --- a/hub/services/static/js/price-calculator.js +++ b/hub/services/static/js/price-calculator.js @@ -1,1899 +1,82 @@ /** - * Price Calculator for Service Offerings - * Handles interactive pricing calculation with sliders and plan selection + * Price Calculator Module Loader + * Loads the modular price calculator components + * The original monolithic code has been split into multiple files for better maintainability */ -class PriceCalculator { - constructor() { - this.pricingData = null; - this.storagePrice = null; - this.currentOffering = null; - this.selectedConfiguration = null; - this.replicaInfo = null; - this.addonsData = null; - this.init(); +(function () { + 'use strict'; + + // Check if we're on a page that needs the price calculator + if (!document.getElementById('cpuRange')) { + return; } - // 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'); - - // Addon elements - this.addonsContainer = document.getElementById('addonsContainer'); - this.addonPricingContainer = document.getElementById('addonPricingContainer'); - this.managedServiceIncludesContainer = document.getElementById('managedServiceIncludesContainer'); - - // 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' }); + // Get the current script's directory + const getCurrentScriptPath = () => { + const scripts = document.getElementsByTagName('script'); + // Find the script that contains 'price-calculator.js' + for (let script of scripts) { + if (script.src.includes('price-calculator.js')) { + const scriptPath = script.src; + const lastSlash = scriptPath.lastIndexOf('/'); + return scriptPath.substring(0, lastSlash + 1); } } - } + return '/static/js/'; + }; - // Pre-fill contact form with selected configuration - prefillContactForm() { - if (!this.selectedConfiguration) return; + // Define the modules to load in dependency order + const modules = [ + 'price-calculator/dom-manager.js', + 'price-calculator/pricing-data-manager.js', + 'price-calculator/plan-manager.js', + 'price-calculator/addon-manager.js', + 'price-calculator/ui-manager.js', + 'price-calculator/order-manager.js', + 'price-calculator/price-calculator.js' + ]; - const config = this.selectedConfiguration; + // Helper function to load a script + const loadScript = (src) => { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + }; - // Create configuration summary message - const configMessage = this.generateConfigurationMessage(config); + // Load modules sequentially + const loadModules = async () => { + const basePath = getCurrentScriptPath(); - // 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; - } + for (const module of modules) { + try { + await loadScript(basePath + module); + } catch (error) { + console.error(`Failed to load module: ${module}`, error); } } - } - // 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']); - - // 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); + // Initialize the calculator after modules are loaded + if (window.PriceCalculator) { + window.priceCalculator = new window.PriceCalculator(); } else { - // For BE, min is 1 - this.instancesRange.min = 1; - this.instancesRange.value = Math.max(this.instancesRange.value, 1); + console.error('PriceCalculator class not found after module loading'); } + }; - // 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; + // Start loading modules when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', loadModules); + } else { + loadModules(); } +})(); - // 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.min = "0.25"; - this.cpuRange.max = Math.ceil(maxCpus); - } - - if (maxMemory > 0) { - this.memoryRange.min = "0.25"; - 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 (parseFloat(a.vcpus) !== parseFloat(b.vcpus)) { - return parseFloat(a.vcpus) - parseFloat(b.vcpus); - } - return parseFloat(a.ram) - parseFloat(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 = parseFloat(this.cpuRange?.value || 0.5); - const memory = parseFloat(this.memoryRange?.value || 1); - 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 = parseFloat(this.cpuRange?.value || 0.5); - const memory = parseFloat(this.memoryRange?.value || 1); - 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 = parseFloat(plan.vcpus); - const planMemory = parseFloat(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 = parseFloat(this.cpuRange.value); - const memory = parseFloat(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 (0.5 vCPUs) - if (this.cpuRange) { - this.cpuRange.value = '0.5'; - if (this.cpuValue) this.cpuValue.textContent = '0.5'; - } - - // Reset Memory slider to default value (1 GB) - if (this.memoryRange) { - this.memoryRange.value = '1'; - if (this.memoryValue) this.memoryValue.textContent = '1'; - } - - // 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']); - - // 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.min = "0.25"; - this.cpuRange.max = Math.ceil(maxCpus); - } - - if (maxMemory > 0) { - this.memoryRange.min = "0.25"; - 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 (parseFloat(a.vcpus) !== parseFloat(b.vcpus)) { - return parseFloat(a.vcpus) - parseFloat(b.vcpus); - } - return parseFloat(a.ram) - parseFloat(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 = parseFloat(this.cpuRange?.value || 0.5); - const memory = parseFloat(this.memoryRange?.value || 1); - 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 = parseFloat(this.cpuRange?.value || 0.5); - const memory = parseFloat(this.memoryRange?.value || 1); - 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 = parseFloat(plan.vcpus); - const planMemory = parseFloat(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 = parseFloat(this.cpuRange.value); - const memory = parseFloat(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; - } - } -} - -// Override the updateAddonPricingDisplay method to use improved layout -PriceCalculator.prototype.updateAddonPricingDisplay = function (mandatoryAddons, selectedOptionalAddons) { - // Handle mandatory addons - show them in the managed service includes section - if (this.managedServiceIncludesContainer) { - this.managedServiceIncludesContainer.innerHTML = ''; - - if (mandatoryAddons && mandatoryAddons.length > 0) { - mandatoryAddons.forEach(addon => { - const addonRow = document.createElement('div'); - addonRow.className = 'd-flex justify-content-between mb-1 small text-success'; - addonRow.innerHTML = ` - ${addon.name} - (CHF ${addon.price}) - `; - this.managedServiceIncludesContainer.appendChild(addonRow); - }); - } else { - // Show basic compute resources when no specific addons - const basicRow = document.createElement('div'); - basicRow.className = 'small text-success mb-1'; - basicRow.innerHTML = ` - Compute resources & management - `; - this.managedServiceIncludesContainer.appendChild(basicRow); - } - } - - // Handle optional addons - show them in the main addon pricing container - if (!this.addonPricingContainer) return; - - // Clear existing addon pricing display (only for optional addons now) - this.addonPricingContainer.innerHTML = ''; - - // Add optional addons to pricing breakdown (these are added to total) - if (selectedOptionalAddons && selectedOptionalAddons.length > 0) { - // Add a header for optional addons - const optionalHeader = document.createElement('div'); - optionalHeader.className = 'small text-muted mb-2'; - optionalHeader.innerHTML = 'Additional add-ons:'; - this.addonPricingContainer.appendChild(optionalHeader); - - 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); - }); - } -}; - -// 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(); - } -}); - +// Global function for traditional plan selection (used by template buttons) function selectPlan(element) { const planId = element.getAttribute('data-plan-id'); const planName = element.getAttribute('data-plan-name'); diff --git a/hub/services/static/js/price-calculator/addon-manager.js b/hub/services/static/js/price-calculator/addon-manager.js new file mode 100644 index 0000000..daf4735 --- /dev/null +++ b/hub/services/static/js/price-calculator/addon-manager.js @@ -0,0 +1,176 @@ +/** + * Addon Manager - Handles addon functionality + */ +class AddonManager { + constructor(pricingDataManager) { + this.pricingDataManager = pricingDataManager; + } + + // Update addons based on current configuration + updateAddons(domManager) { + const addonsContainer = domManager.get('addonsContainer'); + const addonsData = this.pricingDataManager.getAddonsData(); + + if (!addonsContainer || !addonsData) { + // Hide addons section if no container or data + const addonsSection = document.getElementById('addonsSection'); + if (addonsSection) addonsSection.style.display = 'none'; + return; + } + + const serviceLevel = domManager.getSelectedServiceLevel(); + if (!serviceLevel || !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 = addonsData[serviceLevel]; + + // Clear existing addons + 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 = ` +
+ + +
+ `; + + 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(domManager); + // Trigger pricing update through custom event + window.dispatchEvent(new CustomEvent('addon-changed')); + }); + } + }); + + // Update addon prices + this.updateAddonPrices(domManager); + } + + // Update addon prices based on current configuration + updateAddonPrices(domManager, planManager) { + const addonsContainer = domManager.get('addonsContainer'); + if (!addonsContainer) return; + + const config = domManager.getCurrentConfiguration(); + + // Find the current plan data to get variable_unit for addon calculations + const matchedPlan = planManager ? planManager.getCurrentPlan(domManager) : null; + const variableUnit = matchedPlan?.variable_unit || 'CPU'; + const units = variableUnit === 'CPU' ? config.cpus : config.memory; + const totalUnits = units * config.instances; + + const addonCheckboxes = 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) * config.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 selected addons with their calculated prices + getSelectedAddons(domManager) { + const addonsContainer = domManager.get('addonsContainer'); + if (!addonsContainer) return { mandatory: [], optional: [] }; + + const mandatoryAddons = []; + const selectedOptionalAddons = []; + + const addonCheckboxes = 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) { + mandatoryAddons.push({ + name: addon.name, + price: calculatedPrice.toFixed(2) + }); + } else if (checkbox.checked) { + selectedOptionalAddons.push({ + name: addon.name, + price: calculatedPrice.toFixed(2) + }); + } + }); + + return { + mandatory: mandatoryAddons, + optional: selectedOptionalAddons + }; + } + + // Calculate total optional addon price + calculateOptionalAddonTotal(domManager) { + const addonsContainer = domManager.get('addonsContainer'); + if (!addonsContainer) return 0; + + let total = 0; + const addonCheckboxes = addonsContainer.querySelectorAll('.addon-checkbox'); + + addonCheckboxes.forEach(checkbox => { + const addon = JSON.parse(checkbox.dataset.addon); + if (!addon.is_mandatory && checkbox.checked) { + const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0); + total += calculatedPrice; + } + }); + + return total; + } +} + +// Export for use in other modules +window.AddonManager = AddonManager; diff --git a/hub/services/static/js/price-calculator/dom-manager.js b/hub/services/static/js/price-calculator/dom-manager.js new file mode 100644 index 0000000..42c4626 --- /dev/null +++ b/hub/services/static/js/price-calculator/dom-manager.js @@ -0,0 +1,160 @@ +/** + * DOM Manager - Handles DOM element references and basic manipulation + */ +class DOMManager { + constructor() { + this.elements = {}; + this.initElements(); + } + + // Initialize DOM element references + initElements() { + // Calculator controls + this.elements.cpuRange = document.getElementById('cpuRange'); + this.elements.memoryRange = document.getElementById('memoryRange'); + this.elements.storageRange = document.getElementById('storageRange'); + this.elements.instancesRange = document.getElementById('instancesRange'); + this.elements.cpuValue = document.getElementById('cpuValue'); + this.elements.memoryValue = document.getElementById('memoryValue'); + this.elements.storageValue = document.getElementById('storageValue'); + this.elements.instancesValue = document.getElementById('instancesValue'); + this.elements.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]'); + this.elements.planSelect = document.getElementById('planSelect'); + + // Addon elements + this.elements.addonsContainer = document.getElementById('addonsContainer'); + this.elements.addonPricingContainer = document.getElementById('addonPricingContainer'); + this.elements.managedServiceIncludesContainer = document.getElementById('managedServiceIncludesContainer'); + + // Result display elements + this.elements.planMatchStatus = document.getElementById('planMatchStatus'); + this.elements.selectedPlanDetails = document.getElementById('selectedPlanDetails'); + this.elements.noMatchFound = document.getElementById('noMatchFound'); + + // Plan detail elements + this.elements.planGroup = document.getElementById('planGroup'); + this.elements.planName = document.getElementById('planName'); + this.elements.planDescription = document.getElementById('planDescription'); + this.elements.planCpus = document.getElementById('planCpus'); + this.elements.planMemory = document.getElementById('planMemory'); + this.elements.planInstances = document.getElementById('planInstances'); + this.elements.planServiceLevel = document.getElementById('planServiceLevel'); + this.elements.managedServicePrice = document.getElementById('managedServicePrice'); + this.elements.storagePriceEl = document.getElementById('storagePrice'); + this.elements.storageAmount = document.getElementById('storageAmount'); + this.elements.totalPrice = document.getElementById('totalPrice'); + + // Order button + this.elements.orderButton = document.querySelector('a[href="#order-form"]'); + + // Service level group + this.elements.serviceLevelGroup = document.getElementById('serviceLevelGroup'); + } + + // Get element by key + get(key) { + return this.elements[key]; + } + + // Check if element exists + has(key) { + return this.elements[key] && this.elements[key] !== null; + } + + // Update slider display values (min/max text below sliders) + updateSliderDisplayValues() { + // Update CPU slider display + if (this.elements.cpuRange) { + const cpuMinDisplay = document.getElementById('cpuMinDisplay'); + const cpuMaxDisplay = document.getElementById('cpuMaxDisplay'); + if (cpuMinDisplay) cpuMinDisplay.textContent = this.elements.cpuRange.min; + if (cpuMaxDisplay) cpuMaxDisplay.textContent = this.elements.cpuRange.max; + } + + // Update Memory slider display + if (this.elements.memoryRange) { + const memoryMinDisplay = document.getElementById('memoryMinDisplay'); + const memoryMaxDisplay = document.getElementById('memoryMaxDisplay'); + if (memoryMinDisplay) memoryMinDisplay.textContent = this.elements.memoryRange.min; + if (memoryMaxDisplay) memoryMaxDisplay.textContent = this.elements.memoryRange.max; + } + + // Update Storage slider display + if (this.elements.storageRange) { + const storageMinDisplay = document.getElementById('storageMinDisplay'); + const storageMaxDisplay = document.getElementById('storageMaxDisplay'); + if (storageMinDisplay) storageMinDisplay.textContent = this.elements.storageRange.min; + if (storageMaxDisplay) storageMaxDisplay.textContent = this.elements.storageRange.max; + } + + // Update Instances slider display + if (this.elements.instancesRange) { + const instancesMinDisplay = document.getElementById('instancesMinDisplay'); + const instancesMaxDisplay = document.getElementById('instancesMaxDisplay'); + if (instancesMinDisplay) instancesMinDisplay.textContent = this.elements.instancesRange.min; + if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.elements.instancesRange.max; + } + } + + // Get slider container element by type + getSliderContainer(type) { + switch (type) { + case 'cpu': + return this.elements.cpuRange?.closest('.mb-4'); + case 'memory': + return this.elements.memoryRange?.closest('.mb-4'); + case 'storage': + return this.elements.storageRange?.closest('.mb-4'); + case 'instances': + return this.elements.instancesRange?.closest('.mb-4'); + default: + return null; + } + } + + // Reset sliders to their default values + resetSlidersToDefaults() { + // Reset CPU slider to default value (0.5 vCPUs) + if (this.elements.cpuRange) { + this.elements.cpuRange.value = '0.5'; + if (this.elements.cpuValue) this.elements.cpuValue.textContent = '0.5'; + } + + // Reset Memory slider to default value (1 GB) + if (this.elements.memoryRange) { + this.elements.memoryRange.value = '1'; + if (this.elements.memoryValue) this.elements.memoryValue.textContent = '1'; + } + + // Reset Storage slider to default value (20 GB) + if (this.elements.storageRange) { + this.elements.storageRange.value = '20'; + if (this.elements.storageValue) this.elements.storageValue.textContent = '20'; + } + + // Reset Instances slider to default value (1) + if (this.elements.instancesRange) { + this.elements.instancesRange.value = '1'; + if (this.elements.instancesValue) this.elements.instancesValue.textContent = '1'; + } + } + + // Get current selected service level + getSelectedServiceLevel() { + return document.querySelector('input[name="serviceLevel"]:checked')?.value; + } + + // Get current configuration values + getCurrentConfiguration() { + return { + cpus: parseFloat(this.elements.cpuRange?.value || 0.5), + memory: parseFloat(this.elements.memoryRange?.value || 1), + storage: parseInt(this.elements.storageRange?.value || 20), + instances: parseInt(this.elements.instancesRange?.value || 1), + serviceLevel: this.getSelectedServiceLevel() + }; + } +} + +// Export for use in other modules +window.DOMManager = DOMManager; diff --git a/hub/services/static/js/price-calculator/order-manager.js b/hub/services/static/js/price-calculator/order-manager.js new file mode 100644 index 0000000..99ea5a8 --- /dev/null +++ b/hub/services/static/js/price-calculator/order-manager.js @@ -0,0 +1,113 @@ +/** + * Order Manager - Handles order form functionality + */ +class OrderManager { + constructor() { + this.selectedConfiguration = null; + } + + // Setup order button click handler + setupOrderButton(domManager) { + const orderButton = domManager.get('orderButton'); + if (orderButton) { + 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; + } + + // Store current configuration for order button + storeConfiguration(plan, config, serviceLevel, totalPrice, addons) { + this.selectedConfiguration = { + planName: plan.compute_plan, + planGroup: plan.groupName, + vcpus: plan.vcpus, + memory: plan.ram, + storage: config.storage, + instances: config.instances, + serviceLevel: serviceLevel, + totalPrice: totalPrice, + addons: addons + }; + } + + // Get stored configuration + getStoredConfiguration() { + return this.selectedConfiguration; + } +} + +// Export for use in other modules +window.OrderManager = OrderManager; diff --git a/hub/services/static/js/price-calculator/plan-manager.js b/hub/services/static/js/price-calculator/plan-manager.js new file mode 100644 index 0000000..9abdc0f --- /dev/null +++ b/hub/services/static/js/price-calculator/plan-manager.js @@ -0,0 +1,104 @@ +/** + * Plan Manager - Handles plan selection and matching logic + */ +class PlanManager { + constructor(pricingDataManager) { + this.pricingDataManager = pricingDataManager; + } + + // Find best matching plan based on requirements + findBestMatchingPlan(cpus, memory, serviceLevel) { + const pricingData = this.pricingDataManager.getPricingData(); + + if (!pricingData) return null; + + let bestMatch = null; + let bestScore = Infinity; + + // Iterate through all groups and service levels + Object.keys(pricingData).forEach(groupName => { + const group = pricingData[groupName]; + + if (group[serviceLevel]) { + group[serviceLevel].forEach(plan => { + const planCpus = parseFloat(plan.vcpus); + const planMemory = parseFloat(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; + } + + // Get current plan based on configuration + getCurrentPlan(domManager) { + const config = domManager.getCurrentConfiguration(); + const planSelect = domManager.get('planSelect'); + + if (planSelect?.value) { + return JSON.parse(planSelect.value); + } + + return this.findBestMatchingPlan(config.cpus, config.memory, config.serviceLevel); + } + + // Populate plan dropdown based on selected service level + populatePlanDropdown(domManager) { + const planSelect = domManager.get('planSelect'); + if (!planSelect) return; + + const serviceLevel = domManager.getSelectedServiceLevel(); + if (!serviceLevel) return; + + // Clear existing options + planSelect.innerHTML = ''; + + // Get plans for the selected service level + const availablePlans = this.pricingDataManager.getPlansForServiceLevel(serviceLevel); + + // 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`; + planSelect.appendChild(option); + }); + } + + // Update sliders to match selected plan + updateSlidersForPlan(plan, domManager) { + const cpuRange = domManager.get('cpuRange'); + const memoryRange = domManager.get('memoryRange'); + const cpuValue = domManager.get('cpuValue'); + const memoryValue = domManager.get('memoryValue'); + + if (cpuRange && cpuValue) { + cpuRange.value = plan.vcpus; + cpuValue.textContent = plan.vcpus; + } + + if (memoryRange && memoryValue) { + memoryRange.value = plan.ram; + memoryValue.textContent = plan.ram; + } + } +} + +// Export for use in other modules +window.PlanManager = PlanManager; diff --git a/hub/services/static/js/price-calculator/price-calculator.js b/hub/services/static/js/price-calculator/price-calculator.js new file mode 100644 index 0000000..ecde2ac --- /dev/null +++ b/hub/services/static/js/price-calculator/price-calculator.js @@ -0,0 +1,252 @@ +/** + * Price Calculator - Main orchestrator class + * Coordinates all the different managers to provide pricing calculation functionality + */ +class PriceCalculator { + constructor() { + // Initialize managers + this.domManager = new DOMManager(); + this.currentOffering = this.extractOfferingFromURL(); + this.pricingDataManager = new PricingDataManager(this.currentOffering); + this.planManager = new PlanManager(this.pricingDataManager); + this.addonManager = new AddonManager(this.pricingDataManager); + this.uiManager = new UIManager(); + this.orderManager = new OrderManager(); + + // Initialize the calculator + this.init(); + } + + // Extract offering info from URL + extractOfferingFromURL() { + const pathParts = window.location.pathname.split('/'); + if (pathParts.length >= 4 && pathParts[1] === 'offering') { + return { + provider_slug: pathParts[2], + service_slug: pathParts[3] + }; + } + return null; + } + + // Initialize calculator + async init() { + try { + // Load pricing data and setup calculator + if (this.currentOffering) { + await this.pricingDataManager.loadPricingData(); + + this.setupEventListeners(); + this.setupUI(); + this.orderManager.setupOrderButton(this.domManager); + this.updateCalculator(); + } else { + console.warn('No current offering found, calculator not initialized'); + } + } catch (error) { + console.error('Error initializing price calculator:', error); + this.uiManager.showError(this.domManager, 'Failed to load pricing information'); + } + } + + // Setup initial UI components + setupUI() { + // Setup service levels based on available data + this.uiManager.setupServiceLevels(this.domManager, this.pricingDataManager); + + // Calculate and set slider maximums + this.uiManager.updateSliderMaximums(this.domManager, this.pricingDataManager); + + // Populate plan dropdown + this.planManager.populatePlanDropdown(this.domManager); + + // Initialize instances slider + this.uiManager.updateInstancesSlider(this.domManager, this.pricingDataManager); + } + + // Setup event listeners for calculator controls + setupEventListeners() { + const cpuRange = this.domManager.get('cpuRange'); + const memoryRange = this.domManager.get('memoryRange'); + const storageRange = this.domManager.get('storageRange'); + const instancesRange = this.domManager.get('instancesRange'); + + if (!cpuRange || !memoryRange || !storageRange || !instancesRange) return; + + // Slider event listeners + cpuRange.addEventListener('input', () => { + this.domManager.get('cpuValue').textContent = cpuRange.value; + this.updatePricing(); + }); + + memoryRange.addEventListener('input', () => { + this.domManager.get('memoryValue').textContent = memoryRange.value; + this.updatePricing(); + }); + + storageRange.addEventListener('input', () => { + this.domManager.get('storageValue').textContent = storageRange.value; + this.updatePricing(); + }); + + instancesRange.addEventListener('input', () => { + this.domManager.get('instancesValue').textContent = instancesRange.value; + this.updatePricing(); + }); + + // Service level change listeners + const serviceLevelInputs = this.domManager.get('serviceLevelInputs'); + serviceLevelInputs.forEach(input => { + input.addEventListener('change', () => { + this.uiManager.updateInstancesSlider(this.domManager, this.pricingDataManager); + this.planManager.populatePlanDropdown(this.domManager); + this.addonManager.updateAddons(this.domManager); + this.updatePricing(); + }); + }); + + // Plan selection listener + const planSelect = this.domManager.get('planSelect'); + if (planSelect) { + planSelect.addEventListener('change', () => { + if (planSelect.value) { + const selectedPlan = JSON.parse(planSelect.value); + + // Update sliders to match selected plan + this.planManager.updateSlidersForPlan(selectedPlan, this.domManager); + + // Fade out CPU and Memory sliders since plan is manually selected + this.uiManager.fadeOutSliders(this.domManager, ['cpu', 'memory']); + + // Update addons for the new configuration + this.addonManager.updateAddons(this.domManager); + + // Update pricing with the selected plan + this.updatePricingWithPlan(selectedPlan); + } else { + // Auto-select mode - reset sliders to default values + this.domManager.resetSlidersToDefaults(); + + // Auto-select mode - fade sliders back in + this.uiManager.fadeInSliders(this.domManager, ['cpu', 'memory']); + + // Auto-select mode - update addons and recalculate + this.addonManager.updateAddons(this.domManager); + this.updatePricing(); + } + }); + } + + // Listen for addon changes + window.addEventListener('addon-changed', () => { + this.updatePricing(); + }); + } + + // Update calculator (initial setup) + updateCalculator() { + this.addonManager.updateAddons(this.domManager); + this.updatePricing(); + } + + // Update pricing with specific plan + updatePricingWithPlan(selectedPlan) { + const config = this.domManager.getCurrentConfiguration(); + + // Update addon prices first to ensure calculated prices are current + this.addonManager.updateAddonPrices(this.domManager, this.planManager); + + this.showPlanDetails(selectedPlan, config.storage, config.instances); + this.uiManager.updateStatusMessage(this.domManager, 'Plan selected directly!', 'success'); + } + + // Main pricing update function + updatePricing() { + // Update addon prices first to ensure they're current + this.addonManager.updateAddonPrices(this.domManager, this.planManager); + + const planSelect = this.domManager.get('planSelect'); + + // Reset plan selection if in auto-select mode + if (!planSelect?.value) { + const config = this.domManager.getCurrentConfiguration(); + + if (!config.serviceLevel) { + return; + } + + // Find best matching plan + const matchedPlan = this.planManager.findBestMatchingPlan(config.cpus, config.memory, config.serviceLevel); + + if (matchedPlan) { + this.showPlanDetails(matchedPlan, config.storage, config.instances); + this.uiManager.updateStatusMessage(this.domManager, 'Perfect match found!', 'success'); + } else { + this.uiManager.showNoMatch(this.domManager); + } + } else { + // Plan is directly selected, update storage pricing + const selectedPlan = JSON.parse(planSelect.value); + const config = this.domManager.getCurrentConfiguration(); + + // Update addon prices for current configuration + this.addonManager.updateAddonPrices(this.domManager, this.planManager); + this.showPlanDetails(selectedPlan, config.storage, config.instances); + this.uiManager.updateStatusMessage(this.domManager, 'Plan selected directly!', 'success'); + } + } + + // Show plan details in the UI + showPlanDetails(plan, storage, instances) { + // Get current service level + const serviceLevel = this.domManager.getSelectedServiceLevel() || 'Best Effort'; + + // Ensure addon prices are calculated with current configuration + this.addonManager.updateAddonPrices(this.domManager, this.planManager); + + // Calculate pricing using final price from plan data (which already includes mandatory addons) + const managedServicePricePerInstance = parseFloat(plan.final_price); + + // Collect addon information for display and calculation + const addons = this.addonManager.getSelectedAddons(this.domManager); + const optionalAddonTotal = this.addonManager.calculateOptionalAddonTotal(this.domManager); + + 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.pricingDataManager.getStoragePrice(); + const storagePriceValue = storage * storageUnitPrice * instances; + + // Total price = managed service price (includes mandatory addons) + storage + optional addons + const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal; + + // Show plan details in UI + this.uiManager.showPlanDetails( + this.domManager, + plan, + storage, + instances, + serviceLevel, + managedServicePrice, + storagePriceValue, + totalPriceValue + ); + + // Update addon pricing display + this.uiManager.updateAddonPricingDisplay(this.domManager, addons.mandatory, addons.optional); + + // Store current configuration for order button + this.orderManager.storeConfiguration( + plan, + { storage, instances }, + serviceLevel, + totalPriceValue.toFixed(2), + [...addons.mandatory, ...addons.optional] + ); + } +} + +// Export for use in other modules +window.PriceCalculator = PriceCalculator; diff --git a/hub/services/static/js/price-calculator/pricing-data-manager.js b/hub/services/static/js/price-calculator/pricing-data-manager.js new file mode 100644 index 0000000..147086d --- /dev/null +++ b/hub/services/static/js/price-calculator/pricing-data-manager.js @@ -0,0 +1,190 @@ +/** + * Pricing Data Manager - Handles API calls and data extraction + */ +class PricingDataManager { + constructor(currentOffering) { + this.currentOffering = currentOffering; + this.pricingData = null; + this.storagePrice = null; + this.replicaInfo = null; + this.addonsData = null; + } + + // 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: ${response.status} ${response.statusText}`); + } + + 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(); + + return this.pricingData; + } catch (error) { + console.error('Error loading pricing data:', error); + throw error; + } + } + + // 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; + } + }); + }); + } + + // Get available service levels from pricing data + getAvailableServiceLevels() { + if (!this.pricingData) return new Set(); + + const availableServiceLevels = new Set(); + Object.keys(this.pricingData).forEach(groupName => { + const group = this.pricingData[groupName]; + Object.keys(group).forEach(serviceLevel => { + availableServiceLevels.add(serviceLevel); + }); + }); + + return availableServiceLevels; + } + + // Get maximum CPU and memory values from all plans + getSliderMaximums() { + if (!this.pricingData) return { maxCpus: 0, maxMemory: 0 }; + + 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; + }); + }); + }); + + return { maxCpus, maxMemory }; + } + + // Get all plans for a specific service level + getPlansForServiceLevel(serviceLevel) { + if (!this.pricingData || !serviceLevel) return []; + + 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 (parseFloat(a.vcpus) !== parseFloat(b.vcpus)) { + return parseFloat(a.vcpus) - parseFloat(b.vcpus); + } + return parseFloat(a.ram) - parseFloat(b.ram); + }); + + return availablePlans; + } + + // Getters + getPricingData() { + return this.pricingData; + } + + getStoragePrice() { + return this.storagePrice; + } + + getReplicaInfo() { + return this.replicaInfo; + } + + getAddonsData() { + return this.addonsData; + } +} + +// Export for use in other modules +window.PricingDataManager = PricingDataManager; diff --git a/hub/services/static/js/price-calculator/ui-manager.js b/hub/services/static/js/price-calculator/ui-manager.js new file mode 100644 index 0000000..e1a099a --- /dev/null +++ b/hub/services/static/js/price-calculator/ui-manager.js @@ -0,0 +1,269 @@ +/** + * UI Manager - Handles UI updates and visual feedback + */ +class UIManager { + constructor() { + // Visual feedback states + this.isSlidersFaded = false; + } + + // Update status message + updateStatusMessage(domManager, message, type) { + const planMatchStatus = domManager.get('planMatchStatus'); + if (!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'; + + planMatchStatus.innerHTML = `${message}`; + planMatchStatus.className = `alert ${alertClass} mb-3`; + planMatchStatus.style.display = 'block'; + } + + // Show error message + showError(domManager, message) { + const planMatchStatus = domManager.get('planMatchStatus'); + if (planMatchStatus) { + planMatchStatus.innerHTML = `${message}`; + planMatchStatus.className = 'alert alert-danger mb-3'; + planMatchStatus.style.display = 'block'; + } + } + + // Show no matching plan found + showNoMatch(domManager) { + const planMatchStatus = domManager.get('planMatchStatus'); + const selectedPlanDetails = domManager.get('selectedPlanDetails'); + const noMatchFound = domManager.get('noMatchFound'); + + if (planMatchStatus) planMatchStatus.style.display = 'none'; + if (selectedPlanDetails) selectedPlanDetails.style.display = 'none'; + if (noMatchFound) noMatchFound.style.display = 'block'; + } + + // Show plan details in the UI + showPlanDetails(domManager, plan, storage, instances, serviceLevel, managedServicePrice, storagePriceValue, totalPriceValue) { + const selectedPlanDetails = domManager.get('selectedPlanDetails'); + if (!selectedPlanDetails) return; + + // Show plan details section + const planMatchStatus = domManager.get('planMatchStatus'); + const noMatchFound = domManager.get('noMatchFound'); + + if (planMatchStatus) planMatchStatus.style.display = 'block'; + selectedPlanDetails.style.display = 'block'; + if (noMatchFound) noMatchFound.style.display = 'none'; + + // Update plan information + const planGroup = domManager.get('planGroup'); + const planName = domManager.get('planName'); + const planDescription = domManager.get('planDescription'); + const planCpus = domManager.get('planCpus'); + const planMemory = domManager.get('planMemory'); + const planInstances = domManager.get('planInstances'); + const planServiceLevel = domManager.get('planServiceLevel'); + const managedServicePriceEl = domManager.get('managedServicePrice'); + const storagePriceEl = domManager.get('storagePriceEl'); + const storageAmount = domManager.get('storageAmount'); + const totalPrice = domManager.get('totalPrice'); + + if (planGroup) planGroup.textContent = plan.groupName; + if (planName) planName.textContent = plan.compute_plan; + if (planDescription) planDescription.textContent = plan.compute_plan_group_description || ''; + if (planCpus) planCpus.textContent = plan.vcpus; + if (planMemory) planMemory.textContent = plan.ram + ' GB'; + if (planInstances) planInstances.textContent = instances; + if (planServiceLevel) planServiceLevel.textContent = serviceLevel; + + // Update pricing display + if (managedServicePriceEl) managedServicePriceEl.textContent = managedServicePrice.toFixed(2); + if (storagePriceEl) storagePriceEl.textContent = storagePriceValue.toFixed(2); + if (storageAmount) storageAmount.textContent = storage; + if (totalPrice) totalPrice.textContent = totalPriceValue.toFixed(2); + } + + // Update addon pricing display in the results panel + updateAddonPricingDisplay(domManager, mandatoryAddons, selectedOptionalAddons) { + // Update mandatory addons in the managed service includes container + const managedServiceIncludesContainer = domManager.get('managedServiceIncludesContainer'); + if (managedServiceIncludesContainer) { + // Clear existing content + managedServiceIncludesContainer.innerHTML = ''; + + // Add mandatory addons to the managed service includes section + if (mandatoryAddons && mandatoryAddons.length > 0) { + mandatoryAddons.forEach(addon => { + const addonRow = document.createElement('div'); + addonRow.className = 'd-flex justify-content-between small text-muted mb-1'; + addonRow.innerHTML = ` + ${addon.name} + CHF ${addon.price} + `; + managedServiceIncludesContainer.appendChild(addonRow); + }); + } + } + + // Update optional addons in the addon pricing container + const addonPricingContainer = domManager.get('addonPricingContainer'); + if (!addonPricingContainer) return; + + // Clear existing addon pricing display + addonPricingContainer.innerHTML = ''; + + // 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} + `; + addonPricingContainer.appendChild(addonRow); + }); + } + } + + // Fade out specified sliders when plan is manually selected + fadeOutSliders(domManager, sliderTypes) { + sliderTypes.forEach(type => { + const sliderContainer = domManager.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'; + } + } + }); + this.isSlidersFaded = true; + } + + // Fade in specified sliders when auto-select mode is chosen + fadeInSliders(domManager, sliderTypes) { + sliderTypes.forEach(type => { + const sliderContainer = domManager.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'; + } + } + }); + this.isSlidersFaded = false; + } + + // Setup service levels dynamically from pricing data + setupServiceLevels(domManager, pricingDataManager) { + const serviceLevelGroup = domManager.get('serviceLevelGroup'); + if (!serviceLevelGroup) return; + + // Get all available service levels from the pricing data + const availableServiceLevels = pricingDataManager.getAvailableServiceLevels(); + + // 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; + + serviceLevelGroup.appendChild(input); + serviceLevelGroup.appendChild(label); + }); + + // Update the serviceLevelInputs reference + domManager.elements.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]'); + } + + // Update slider maximums based on pricing data + updateSliderMaximums(domManager, pricingDataManager) { + const cpuRange = domManager.get('cpuRange'); + const memoryRange = domManager.get('memoryRange'); + + if (!cpuRange || !memoryRange) return; + + const { maxCpus, maxMemory } = pricingDataManager.getSliderMaximums(); + + // Set slider maximums with some padding + if (maxCpus > 0) { + cpuRange.min = "0.25"; + cpuRange.max = Math.ceil(maxCpus); + } + + if (maxMemory > 0) { + memoryRange.min = "0.25"; + memoryRange.max = Math.ceil(maxMemory); + } + + // Update display values after changing min/max + domManager.updateSliderDisplayValues(); + } + + // Update instances slider based on service level and replica info + updateInstancesSlider(domManager, pricingDataManager) { + const instancesRange = domManager.get('instancesRange'); + const instancesValue = domManager.get('instancesValue'); + const replicaInfo = pricingDataManager.getReplicaInfo(); + + if (!instancesRange || !replicaInfo) return; + + const serviceLevel = domManager.getSelectedServiceLevel(); + + if (serviceLevel === 'Guaranteed Availability') { + // For GA, min is ha_replica_min + instancesRange.min = replicaInfo.ha_replica_min; + instancesRange.value = Math.max(instancesRange.value, replicaInfo.ha_replica_min); + } else { + // For BE, min is 1 + instancesRange.min = 1; + instancesRange.value = Math.max(instancesRange.value, 1); + } + + // Set max to ha_replica_max + instancesRange.max = replicaInfo.ha_replica_max; + + // Update display value + if (instancesValue) instancesValue.textContent = instancesRange.value; + + // Update the min/max display under the slider + const instancesMinDisplay = document.getElementById('instancesMinDisplay'); + const instancesMaxDisplay = document.getElementById('instancesMaxDisplay'); + + if (instancesMinDisplay) instancesMinDisplay.textContent = instancesRange.min; + if (instancesMaxDisplay) instancesMaxDisplay.textContent = instancesRange.max; + } +} + +// Export for use in other modules +window.UIManager = UIManager;