diff --git a/hub/services/static/js/price-calculator.js b/hub/services/static/js/price-calculator.js
index 96069fe..54b2af9 100644
--- a/hub/services/static/js/price-calculator.js
+++ b/hub/services/static/js/price-calculator.js
@@ -343,6 +343,880 @@ Please contact me with next steps for ordering this configuration.`;
// Update pricing with the selected plan
this.updatePricingWithPlan(selectedPlan);
} else {
+ // Auto-select mode - reset sliders to default values
+ this.resetSlidersToDefaults();
+
+ // Auto-select mode - fade sliders back in
+ this.fadeInSliders(['cpu', 'memory']);
+
+ // Auto-select mode - update addons and recalculate
+ this.updateAddons();
+ this.updatePricing();
+ }
+ });
+ }
+
+ // Initialize instances slider
+ this.updateInstancesSlider();
+ }
+
+ // Update instances slider based on service level and replica info
+ updateInstancesSlider() {
+ if (!this.instancesRange || !this.replicaInfo) return;
+
+ const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
+
+ if (serviceLevel === 'Guaranteed Availability') {
+ // For GA, min is ha_replica_min
+ this.instancesRange.min = this.replicaInfo.ha_replica_min;
+ this.instancesRange.value = Math.max(this.instancesRange.value, this.replicaInfo.ha_replica_min);
+ } else {
+ // For BE, min is 1
+ this.instancesRange.min = 1;
+ this.instancesRange.value = Math.max(this.instancesRange.value, 1);
+ }
+
+ // Set max to ha_replica_max
+ this.instancesRange.max = this.replicaInfo.ha_replica_max;
+
+ // Update display value
+ this.instancesValue.textContent = this.instancesRange.value;
+
+ // Update the min/max display under the slider using direct IDs
+ const instancesMinDisplay = document.getElementById('instancesMinDisplay');
+ const instancesMaxDisplay = document.getElementById('instancesMaxDisplay');
+
+ if (instancesMinDisplay) instancesMinDisplay.textContent = this.instancesRange.min;
+ if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.instancesRange.max;
+ }
+
+ // Setup service levels dynamically from pricing data
+ setupServiceLevels() {
+ if (!this.pricingData) return;
+
+ const serviceLevelGroup = document.getElementById('serviceLevelGroup');
+ if (!serviceLevelGroup) return;
+
+ // Get all available service levels from the pricing data
+ const availableServiceLevels = new Set();
+ Object.keys(this.pricingData).forEach(groupName => {
+ const group = this.pricingData[groupName];
+ Object.keys(group).forEach(serviceLevel => {
+ availableServiceLevels.add(serviceLevel);
+ });
+ });
+
+ // Clear existing service level buttons
+ serviceLevelGroup.innerHTML = '';
+
+ // Create buttons for each available service level
+ let isFirst = true;
+ availableServiceLevels.forEach(serviceLevel => {
+ const inputId = `serviceLevel${serviceLevel.replace(/\s+/g, '')}`;
+
+ // Create radio input
+ const input = document.createElement('input');
+ input.type = 'radio';
+ input.className = 'btn-check';
+ input.name = 'serviceLevel';
+ input.id = inputId;
+ input.value = serviceLevel;
+ if (isFirst) {
+ input.checked = true;
+ isFirst = false;
+ }
+
+ // Create label
+ const label = document.createElement('label');
+ label.className = 'btn btn-outline-primary';
+ label.setAttribute('for', inputId);
+ label.textContent = serviceLevel;
+
+ // Add event listener
+ input.addEventListener('change', () => {
+ this.updateInstancesSlider();
+ this.populatePlanDropdown();
+ this.updateAddons();
+ this.updatePricing();
+ });
+
+ serviceLevelGroup.appendChild(input);
+ serviceLevelGroup.appendChild(label);
+ });
+
+ // Update the serviceLevelInputs reference
+ this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
+
+ // Calculate and set slider maximums based on available plans - this will call updateSliderDisplayValues()
+ this.updateSliderMaximums();
+ }
+
+ // Calculate maximum values for sliders based on available plans
+ updateSliderMaximums() {
+ if (!this.pricingData || !this.cpuRange || !this.memoryRange) return;
+
+ let maxCpus = 0;
+ let maxMemory = 0;
+
+ // Find maximum CPU and memory across all plans
+ Object.keys(this.pricingData).forEach(groupName => {
+ const group = this.pricingData[groupName];
+ Object.keys(group).forEach(serviceLevel => {
+ group[serviceLevel].forEach(plan => {
+ const planCpus = parseFloat(plan.vcpus);
+ const planMemory = parseFloat(plan.ram);
+
+ if (planCpus > maxCpus) maxCpus = planCpus;
+ if (planMemory > maxMemory) maxMemory = planMemory;
+ });
+ });
+ });
+
+ // Set slider maximums with some padding
+ if (maxCpus > 0) {
+ this.cpuRange.max = Math.ceil(maxCpus);
+ }
+
+ if (maxMemory > 0) {
+ this.memoryRange.max = Math.ceil(maxMemory);
+ }
+
+ // Update display values after changing min/max - moved to end and call explicitly
+ this.updateSliderDisplayValues();
+ }
+
+ // Populate plan dropdown based on selected service level
+ populatePlanDropdown() {
+ if (!this.planSelect || !this.pricingData) return;
+
+ const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
+ if (!serviceLevel) return;
+
+ // Clear existing options
+ this.planSelect.innerHTML = '';
+
+ // Collect all plans for selected service level
+ const availablePlans = [];
+ Object.keys(this.pricingData).forEach(groupName => {
+ const group = this.pricingData[groupName];
+ if (group[serviceLevel]) {
+ group[serviceLevel].forEach(plan => {
+ availablePlans.push({
+ ...plan,
+ groupName: groupName
+ });
+ });
+ }
+ });
+
+ // Sort plans by vCPU, then by RAM
+ availablePlans.sort((a, b) => {
+ if (parseInt(a.vcpus) !== parseInt(b.vcpus)) {
+ return parseInt(a.vcpus) - parseInt(b.vcpus);
+ }
+ return parseInt(a.ram) - parseInt(b.ram);
+ });
+
+ // Add plans to dropdown
+ availablePlans.forEach(plan => {
+ const option = document.createElement('option');
+ option.value = JSON.stringify(plan);
+ option.textContent = `${plan.compute_plan} - ${plan.vcpus} vCPUs, ${plan.ram} GB RAM`;
+ this.planSelect.appendChild(option);
+ });
+ }
+
+ // Update addons based on current configuration
+ updateAddons() {
+ if (!this.addonsContainer || !this.addonsData) {
+ // Hide addons section if no container or data
+ const addonsSection = document.getElementById('addonsSection');
+ if (addonsSection) addonsSection.style.display = 'none';
+ return;
+ }
+
+ const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
+ if (!serviceLevel || !this.addonsData[serviceLevel]) {
+ // Hide addons section if no service level or no addons for this level
+ const addonsSection = document.getElementById('addonsSection');
+ if (addonsSection) addonsSection.style.display = 'none';
+ return;
+ }
+
+ const addons = this.addonsData[serviceLevel];
+
+ // Clear existing addons
+ this.addonsContainer.innerHTML = '';
+
+ // Show or hide addons section based on availability
+ const addonsSection = document.getElementById('addonsSection');
+ if (addons && addons.length > 0) {
+ if (addonsSection) addonsSection.style.display = 'block';
+ } else {
+ if (addonsSection) addonsSection.style.display = 'none';
+ return;
+ }
+
+ // Add each addon
+ addons.forEach(addon => {
+ const addonElement = document.createElement('div');
+ addonElement.className = `addon-item mb-2 p-2 border rounded ${addon.is_mandatory ? 'bg-light' : ''}`;
+
+ addonElement.innerHTML = `
+
+ `;
+
+ this.addonsContainer.appendChild(addonElement);
+
+ // Add event listener for optional addons
+ if (!addon.is_mandatory) {
+ const checkbox = addonElement.querySelector('.addon-checkbox');
+ checkbox.addEventListener('change', () => {
+ // Update addon prices and recalculate total
+ this.updateAddonPrices();
+ this.updatePricing();
+ });
+ }
+ });
+
+ // Update addon prices
+ this.updateAddonPrices();
+ } // Update addon prices based on current configuration
+ updateAddonPrices() {
+ if (!this.addonsContainer) return;
+
+ const cpus = parseInt(this.cpuRange?.value || 2);
+ const memory = parseInt(this.memoryRange?.value || 4);
+ const storage = parseInt(this.storageRange?.value || 20);
+ const instances = parseInt(this.instancesRange?.value || 1);
+
+ // Find the current plan data to get variable_unit for addon calculations
+ const matchedPlan = this.getCurrentPlan();
+ const variableUnit = matchedPlan?.variable_unit || 'CPU';
+ const units = variableUnit === 'CPU' ? cpus : memory;
+ const totalUnits = units * instances;
+
+ const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox');
+ addonCheckboxes.forEach(checkbox => {
+ const addon = JSON.parse(checkbox.dataset.addon);
+ const priceElement = checkbox.parentElement.querySelector('.addon-price-value');
+
+ let calculatedPrice = 0;
+
+ // Calculate addon price based on type
+ if (addon.addon_type === 'BASE_FEE') {
+ // Base fee: price per instance
+ calculatedPrice = parseFloat(addon.price || 0) * instances;
+ } else if (addon.addon_type === 'UNIT_RATE') {
+ // Unit rate: price per unit (CPU or memory) across all instances
+ calculatedPrice = parseFloat(addon.price_per_unit || 0) * totalUnits;
+ }
+
+ // Update the display price
+ if (priceElement) {
+ priceElement.textContent = calculatedPrice.toFixed(2);
+ }
+
+ // Store the calculated price for later use in total calculations
+ checkbox.dataset.calculatedPrice = calculatedPrice.toString();
+ });
+ }
+
+ // Get current plan based on configuration
+ getCurrentPlan() {
+ const cpus = parseInt(this.cpuRange?.value || 2);
+ const memory = parseInt(this.memoryRange?.value || 4);
+ const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
+
+ if (this.planSelect?.value) {
+ return JSON.parse(this.planSelect.value);
+ }
+
+ return this.findBestMatchingPlan(cpus, memory, serviceLevel);
+ }
+
+ // Find best matching plan based on requirements
+ findBestMatchingPlan(cpus, memory, serviceLevel) {
+ if (!this.pricingData) return null;
+
+ let bestMatch = null;
+ let bestScore = Infinity;
+
+ // Iterate through all groups and service levels
+ Object.keys(this.pricingData).forEach(groupName => {
+ const group = this.pricingData[groupName];
+
+ if (group[serviceLevel]) {
+ group[serviceLevel].forEach(plan => {
+ const planCpus = parseInt(plan.vcpus);
+ const planMemory = parseInt(plan.ram);
+
+ // Check if plan meets minimum requirements
+ if (planCpus >= cpus && planMemory >= memory) {
+ // Calculate efficiency score (lower is better)
+ const cpuOverhead = planCpus - cpus;
+ const memoryOverhead = planMemory - memory;
+ const score = cpuOverhead + memoryOverhead + plan.final_price * 0.1;
+
+ if (score < bestScore) {
+ bestScore = score;
+ bestMatch = {
+ ...plan,
+ groupName: groupName
+ };
+ }
+ }
+ });
+ }
+ });
+
+ return bestMatch;
+ }
+
+ // Update pricing with specific plan
+ updatePricingWithPlan(selectedPlan) {
+ const storage = parseInt(this.storageRange?.value || 20);
+ const instances = parseInt(this.instancesRange?.value || 1);
+
+ // Update addon prices first to ensure calculated prices are current
+ this.updateAddonPrices();
+
+ this.showPlanDetails(selectedPlan, storage, instances);
+ this.updateStatusMessage('Plan selected directly!', 'success');
+ }
+
+ // Main pricing update function
+ updatePricing() {
+ if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
+
+ // Update addon prices first to ensure they're current
+ this.updateAddonPrices();
+
+ // Reset plan selection if in auto-select mode
+ if (!this.planSelect?.value) {
+ const cpus = parseInt(this.cpuRange.value);
+ const memory = parseInt(this.memoryRange.value);
+ const storage = parseInt(this.storageRange.value);
+ const instances = parseInt(this.instancesRange.value);
+ const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
+
+ if (!serviceLevel) return;
+
+ // Find best matching plan
+ const matchedPlan = this.findBestMatchingPlan(cpus, memory, serviceLevel);
+
+ if (matchedPlan) {
+ this.showPlanDetails(matchedPlan, storage, instances);
+ this.updateStatusMessage('Perfect match found!', 'success');
+ } else {
+ this.showNoMatch();
+ }
+ } else {
+ // Plan is directly selected, update storage pricing
+ const selectedPlan = JSON.parse(this.planSelect.value);
+ const storage = parseInt(this.storageRange.value);
+ const instances = parseInt(this.instancesRange.value);
+
+ // Update addon prices for current configuration
+ this.updateAddonPrices();
+ this.showPlanDetails(selectedPlan, storage, instances);
+ this.updateStatusMessage('Plan selected directly!', 'success');
+ }
+ }
+
+ // Show plan details in the UI
+ showPlanDetails(plan, storage, instances) {
+ if (!this.selectedPlanDetails) return;
+
+ // Show plan details section
+ this.planMatchStatus.style.display = 'block';
+ this.selectedPlanDetails.style.display = 'block';
+ if (this.noMatchFound) this.noMatchFound.style.display = 'none';
+
+ // Get current service level
+ const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value || 'Best Effort';
+
+ // Update plan information
+ if (this.planGroup) this.planGroup.textContent = plan.groupName;
+ if (this.planName) this.planName.textContent = plan.compute_plan;
+ if (this.planDescription) this.planDescription.textContent = plan.compute_plan_group_description || '';
+ if (this.planCpus) this.planCpus.textContent = plan.vcpus;
+ if (this.planMemory) this.planMemory.textContent = plan.ram + ' GB';
+ if (this.planInstances) this.planInstances.textContent = instances;
+ if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel;
+
+ // Ensure addon prices are calculated with current configuration
+ this.updateAddonPrices();
+
+ // Calculate pricing using final price from plan data (which already includes mandatory addons)
+ // plan.final_price = compute_plan_price + sla_price (where sla_price includes mandatory addons)
+ const managedServicePricePerInstance = parseFloat(plan.final_price);
+
+ // Collect addon information for display and calculation
+ let mandatoryAddonTotal = 0;
+ let optionalAddonTotal = 0;
+ const mandatoryAddons = [];
+ const selectedOptionalAddons = [];
+
+ if (this.addonsContainer) {
+ const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox');
+ addonCheckboxes.forEach(checkbox => {
+ const addon = JSON.parse(checkbox.dataset.addon);
+ const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0);
+
+ if (addon.is_mandatory) {
+ // Mandatory addons are already included in plan.final_price
+ // We collect them for display purposes only
+ mandatoryAddons.push({
+ name: addon.name,
+ price: calculatedPrice.toFixed(2)
+ });
+ } else if (checkbox.checked) {
+ // Only count checked optional addons
+ optionalAddonTotal += calculatedPrice;
+ selectedOptionalAddons.push({
+ name: addon.name,
+ price: calculatedPrice.toFixed(2)
+ });
+ }
+ });
+ }
+
+ const managedServicePrice = managedServicePricePerInstance * instances;
+
+ // Use storage price from plan data or fallback to instance variable
+ const storageUnitPrice = plan.storage_price !== undefined ? parseFloat(plan.storage_price) : this.storagePrice;
+ const storagePriceValue = storage * storageUnitPrice * instances;
+
+ // Total price = managed service price (includes mandatory addons) + storage + optional addons
+ const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal;
+
+ // Update pricing display
+ if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2);
+ if (this.storagePriceEl) this.storagePriceEl.textContent = storagePriceValue.toFixed(2);
+ if (this.storageAmount) this.storageAmount.textContent = storage;
+ if (this.totalPrice) this.totalPrice.textContent = totalPriceValue.toFixed(2);
+
+ // Update addon pricing display
+ this.updateAddonPricingDisplay(mandatoryAddons, selectedOptionalAddons);
+
+ // Store current configuration for order button
+ this.selectedConfiguration = {
+ planName: plan.compute_plan,
+ planGroup: plan.groupName,
+ vcpus: plan.vcpus,
+ memory: plan.ram,
+ storage: storage,
+ instances: instances,
+ serviceLevel: serviceLevel,
+ totalPrice: totalPriceValue.toFixed(2),
+ addons: [...mandatoryAddons, ...selectedOptionalAddons]
+ };
+ }
+
+ // Update addon pricing display in the results panel
+ updateAddonPricingDisplay(mandatoryAddons, selectedOptionalAddons) {
+ if (!this.addonPricingContainer) return;
+
+ // Clear existing addon pricing display
+ this.addonPricingContainer.innerHTML = '';
+
+ // Add mandatory addons to pricing breakdown (for informational purposes only)
+ if (mandatoryAddons && mandatoryAddons.length > 0) {
+ // Add a note explaining mandatory addons are included
+ const mandatoryNote = document.createElement('div');
+ mandatoryNote.className = 'text-muted small mb-2';
+ mandatoryNote.innerHTML = 'Required add-ons (included in managed service price):';
+ this.addonPricingContainer.appendChild(mandatoryNote);
+
+ mandatoryAddons.forEach(addon => {
+ const addonRow = document.createElement('div');
+ addonRow.className = 'd-flex justify-content-between mb-1 ps-3';
+ addonRow.innerHTML = `
+ ${addon.name}
+ CHF ${addon.price}
+ `;
+ this.addonPricingContainer.appendChild(addonRow);
+ });
+
+ // Add separator if there are also optional addons
+ if (selectedOptionalAddons && selectedOptionalAddons.length > 0) {
+ const separator = document.createElement('hr');
+ separator.className = 'my-2';
+ this.addonPricingContainer.appendChild(separator);
+ }
+ }
+
+ // Add optional addons to pricing breakdown (these are added to total)
+ if (selectedOptionalAddons && selectedOptionalAddons.length > 0) {
+ selectedOptionalAddons.forEach(addon => {
+ const addonRow = document.createElement('div');
+ addonRow.className = 'd-flex justify-content-between mb-2';
+ addonRow.innerHTML = `
+ Add-on: ${addon.name}
+ CHF ${addon.price}
+ `;
+ this.addonPricingContainer.appendChild(addonRow);
+ });
+ }
+ }
+
+ // Show no matching plan found
+ showNoMatch() {
+ if (this.planMatchStatus) this.planMatchStatus.style.display = 'none';
+ if (this.selectedPlanDetails) this.selectedPlanDetails.style.display = 'none';
+ if (this.noMatchFound) this.noMatchFound.style.display = 'block';
+ }
+
+ // Update status message
+ updateStatusMessage(message, type) {
+ if (!this.planMatchStatus) return;
+
+ const iconClass = type === 'success' ? 'bi-check-circle' : 'bi-info-circle';
+ const textClass = type === 'success' ? 'text-success' : '';
+ const alertClass = type === 'success' ? 'alert-success' : 'alert-info';
+
+ this.planMatchStatus.innerHTML = `${message}`;
+ this.planMatchStatus.className = `alert ${alertClass} mb-3`;
+ this.planMatchStatus.style.display = 'block';
+ }
+
+ // Show error message
+ showError(message) {
+ if (this.planMatchStatus) {
+ this.planMatchStatus.innerHTML = `${message}`;
+ this.planMatchStatus.className = 'alert alert-danger mb-3';
+ this.planMatchStatus.style.display = 'block';
+ }
+ }
+
+ // Fade out specified sliders when plan is manually selected
+ fadeOutSliders(sliderTypes) {
+ sliderTypes.forEach(type => {
+ const sliderContainer = this.getSliderContainer(type);
+ if (sliderContainer) {
+ sliderContainer.style.transition = 'opacity 0.3s ease-in-out';
+ sliderContainer.style.opacity = '0.3';
+ sliderContainer.style.pointerEvents = 'none';
+
+ // Add visual indicator that sliders are disabled
+ const slider = sliderContainer.querySelector('.form-range');
+ if (slider) {
+ slider.style.cursor = 'not-allowed';
+ }
+ }
+ });
+ }
+
+ // Fade in specified sliders when auto-select mode is chosen
+ fadeInSliders(sliderTypes) {
+ sliderTypes.forEach(type => {
+ const sliderContainer = this.getSliderContainer(type);
+ if (sliderContainer) {
+ sliderContainer.style.transition = 'opacity 0.3s ease-in-out';
+ sliderContainer.style.opacity = '1';
+ sliderContainer.style.pointerEvents = 'auto';
+
+ // Remove visual indicator
+ const slider = sliderContainer.querySelector('.form-range');
+ if (slider) {
+ slider.style.cursor = 'pointer';
+ }
+ }
+ });
+ }
+
+ // Get slider container element by type
+ getSliderContainer(type) {
+ switch (type) {
+ case 'cpu':
+ return this.cpuRange?.closest('.mb-4');
+ case 'memory':
+ return this.memoryRange?.closest('.mb-4');
+ case 'storage':
+ return this.storageRange?.closest('.mb-4');
+ case 'instances':
+ return this.instancesRange?.closest('.mb-4');
+ default:
+ return null;
+ }
+ }
+
+ // Reset sliders to their default values
+ resetSlidersToDefaults() {
+ // Reset CPU slider to default value (2)
+ if (this.cpuRange) {
+ this.cpuRange.value = '2';
+ if (this.cpuValue) this.cpuValue.textContent = '2';
+ }
+
+ // Reset Memory slider to default value (4 GB)
+ if (this.memoryRange) {
+ this.memoryRange.value = '4';
+ if (this.memoryValue) this.memoryValue.textContent = '4';
+ }
+
+ // Reset Storage slider to default value (20 GB)
+ if (this.storageRange) {
+ this.storageRange.value = '20';
+ if (this.storageValue) this.storageValue.textContent = '20';
+ }
+
+ // Reset Instances slider to default value (1)
+ if (this.instancesRange) {
+ this.instancesRange.value = '1';
+ if (this.instancesValue) this.instancesValue.textContent = '1';
+ }
+ }
+
+ // Setup order button click handler
+ setupOrderButton() {
+ if (this.orderButton) {
+ this.orderButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.handleOrderClick();
+ });
+ }
+ }
+
+ // Handle order button click
+ handleOrderClick() {
+ if (this.selectedConfiguration) {
+ // Pre-fill the contact form with configuration details
+ this.prefillContactForm();
+
+ // Scroll to the contact form
+ const contactForm = document.getElementById('order-form');
+ if (contactForm) {
+ contactForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ }
+ }
+
+ // Pre-fill contact form with selected configuration
+ prefillContactForm() {
+ if (!this.selectedConfiguration) return;
+
+ const config = this.selectedConfiguration;
+
+ // Create configuration summary message
+ const configMessage = this.generateConfigurationMessage(config);
+
+ // Find and fill the message textarea in the contact form
+ const messageField = document.querySelector('#order-form textarea[name="message"]');
+ if (messageField) {
+ messageField.value = configMessage;
+ }
+
+ // Store configuration details in hidden field
+ const detailsField = document.querySelector('#order-form input[name="details"]');
+ if (detailsField) {
+ detailsField.value = JSON.stringify({
+ plan: config.planName,
+ vcpus: config.vcpus,
+ memory: config.memory,
+ storage: config.storage,
+ instances: config.instances,
+ serviceLevel: config.serviceLevel,
+ totalPrice: config.totalPrice,
+ addons: config.addons || []
+ });
+ }
+ }
+
+ // Generate human-readable configuration message
+ generateConfigurationMessage(config) {
+ let message = `I would like to order the following configuration:
+
+Plan: ${config.planName} (${config.planGroup})
+vCPUs: ${config.vcpus}
+Memory: ${config.memory} GB
+Storage: ${config.storage} GB
+Instances: ${config.instances}
+Service Level: ${config.serviceLevel}`;
+
+ // Add addons to the message if any are selected
+ if (config.addons && config.addons.length > 0) {
+ message += '\n\nSelected Add-ons:';
+ config.addons.forEach(addon => {
+ message += `\n- ${addon.name}: CHF ${addon.price}`;
+ });
+ }
+
+ message += `\n\nTotal Monthly Price: CHF ${config.totalPrice}
+
+Please contact me with next steps for ordering this configuration.`;
+
+ return message;
+ }
+
+ // Load pricing data from API endpoint
+ async loadPricingData() {
+ try {
+ const response = await fetch(`/offering/${this.currentOffering.provider_slug}/${this.currentOffering.service_slug}/?pricing=json`);
+ if (!response.ok) {
+ throw new Error('Failed to load pricing data');
+ }
+
+ const data = await response.json();
+ this.pricingData = data.pricing || data;
+
+ // Extract addons data from the plans - addons are embedded in each plan
+ this.extractAddonsData();
+
+ // Extract storage price from the first available plan
+ this.extractStoragePrice();
+
+ this.setupEventListeners();
+ this.populatePlanDropdown();
+ this.updateAddons();
+ this.updatePricing();
+ } catch (error) {
+ console.error('Error loading pricing data:', error);
+ this.showError('Failed to load pricing information');
+ }
+ }
+
+ // Extract replica information and storage price from pricing data
+ extractStoragePrice() {
+ if (!this.pricingData) return;
+
+ // Find the first plan with storage pricing data and replica info
+ for (const groupName of Object.keys(this.pricingData)) {
+ const group = this.pricingData[groupName];
+ for (const serviceLevel of Object.keys(group)) {
+ const plans = group[serviceLevel];
+ if (plans.length > 0 && plans[0].storage_price !== undefined) {
+ this.storagePrice = parseFloat(plans[0].storage_price);
+ this.replicaInfo = {
+ ha_replica_min: plans[0].ha_replica_min || 1,
+ ha_replica_max: plans[0].ha_replica_max || 1
+ };
+ return;
+ }
+ }
+ }
+ }
+
+ // Extract addons data from pricing plans
+ extractAddonsData() {
+ if (!this.pricingData) return;
+
+ this.addonsData = {};
+
+ // Extract addons from the first available plan for each service level
+ Object.keys(this.pricingData).forEach(groupName => {
+ const group = this.pricingData[groupName];
+ Object.keys(group).forEach(serviceLevel => {
+ const plans = group[serviceLevel];
+ if (plans.length > 0) {
+ // Use the first plan's addon data for this service level
+ const plan = plans[0];
+ const allAddons = [];
+
+ // Add mandatory addons
+ if (plan.mandatory_addons) {
+ plan.mandatory_addons.forEach(addon => {
+ allAddons.push({
+ ...addon,
+ is_mandatory: true,
+ addon_type: addon.addon_type === "Base Fee" ? "BASE_FEE" : "UNIT_RATE"
+ });
+ });
+ }
+
+ // Add optional addons
+ if (plan.optional_addons) {
+ plan.optional_addons.forEach(addon => {
+ allAddons.push({
+ ...addon,
+ is_mandatory: false,
+ addon_type: addon.addon_type === "Base Fee" ? "BASE_FEE" : "UNIT_RATE"
+ });
+ });
+ }
+
+ this.addonsData[serviceLevel] = allAddons;
+ }
+ });
+ });
+ }
+
+ // Setup event listeners for calculator controls
+ setupEventListeners() {
+ if (!this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
+
+ // Setup service levels based on available data
+ this.setupServiceLevels();
+
+ // Slider event listeners
+ this.cpuRange.addEventListener('input', () => {
+ this.cpuValue.textContent = this.cpuRange.value;
+ this.updatePricing();
+ });
+
+ this.memoryRange.addEventListener('input', () => {
+ this.memoryValue.textContent = this.memoryRange.value;
+ this.updatePricing();
+ });
+
+ this.storageRange.addEventListener('input', () => {
+ this.storageValue.textContent = this.storageRange.value;
+ this.updatePricing();
+ });
+
+ this.instancesRange.addEventListener('input', () => {
+ this.instancesValue.textContent = this.instancesRange.value;
+ this.updatePricing();
+ });
+
+ // Service level change listeners
+ this.serviceLevelInputs.forEach(input => {
+ input.addEventListener('change', () => {
+ this.updateInstancesSlider();
+ this.populatePlanDropdown();
+ this.updateAddons();
+ this.updatePricing();
+ });
+ });
+
+ // Plan selection listener
+ if (this.planSelect) {
+ this.planSelect.addEventListener('change', () => {
+ if (this.planSelect.value) {
+ const selectedPlan = JSON.parse(this.planSelect.value);
+
+ // Update sliders to match selected plan
+ this.cpuRange.value = selectedPlan.vcpus;
+ this.memoryRange.value = selectedPlan.ram;
+ this.cpuValue.textContent = selectedPlan.vcpus;
+ this.memoryValue.textContent = selectedPlan.ram;
+
+ // Fade out CPU and Memory sliders since plan is manually selected
+ this.fadeOutSliders(['cpu', 'memory']);
+
+ // Update addons for the new configuration
+ this.updateAddons();
+ // Update pricing with the selected plan
+ this.updatePricingWithPlan(selectedPlan);
+ } else {
+ // Auto-select mode - reset sliders to default values
+ this.resetSlidersToDefaults();
+
// Auto-select mode - fade sliders back in
this.fadeInSliders(['cpu', 'memory']);