612 lines
No EOL
24 KiB
JavaScript
612 lines
No EOL
24 KiB
JavaScript
/**
|
|
* Price Calculator for Service Offerings
|
|
* Handles interactive pricing calculation with sliders and plan selection
|
|
*/
|
|
|
|
class PriceCalculator {
|
|
constructor() {
|
|
this.pricingData = null;
|
|
this.storagePrice = null;
|
|
this.currentOffering = null;
|
|
this.selectedConfiguration = null;
|
|
this.replicaInfo = null;
|
|
this.init();
|
|
}
|
|
|
|
// Initialize calculator elements and event listeners
|
|
init() {
|
|
// Get offering info from URL
|
|
const pathParts = window.location.pathname.split('/');
|
|
if (pathParts.length >= 4 && pathParts[1] === 'offering') {
|
|
this.currentOffering = {
|
|
provider_slug: pathParts[2],
|
|
service_slug: pathParts[3]
|
|
};
|
|
}
|
|
|
|
// Initialize DOM elements
|
|
this.initElements();
|
|
|
|
// Load pricing data and setup calculator
|
|
if (this.currentOffering) {
|
|
this.loadPricingData();
|
|
}
|
|
|
|
// Setup order button click handler
|
|
this.setupOrderButton();
|
|
}
|
|
|
|
// Initialize DOM element references
|
|
initElements() {
|
|
// Calculator controls
|
|
this.cpuRange = document.getElementById('cpuRange');
|
|
this.memoryRange = document.getElementById('memoryRange');
|
|
this.storageRange = document.getElementById('storageRange');
|
|
this.instancesRange = document.getElementById('instancesRange');
|
|
this.cpuValue = document.getElementById('cpuValue');
|
|
this.memoryValue = document.getElementById('memoryValue');
|
|
this.storageValue = document.getElementById('storageValue');
|
|
this.instancesValue = document.getElementById('instancesValue');
|
|
this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
|
|
this.planSelect = document.getElementById('planSelect');
|
|
|
|
// Result display elements
|
|
this.planMatchStatus = document.getElementById('planMatchStatus');
|
|
this.selectedPlanDetails = document.getElementById('selectedPlanDetails');
|
|
this.noMatchFound = document.getElementById('noMatchFound');
|
|
|
|
// Plan detail elements
|
|
this.planGroup = document.getElementById('planGroup');
|
|
this.planName = document.getElementById('planName');
|
|
this.planDescription = document.getElementById('planDescription');
|
|
this.planCpus = document.getElementById('planCpus');
|
|
this.planMemory = document.getElementById('planMemory');
|
|
this.planInstances = document.getElementById('planInstances');
|
|
this.planServiceLevel = document.getElementById('planServiceLevel');
|
|
this.managedServicePrice = document.getElementById('managedServicePrice');
|
|
this.storagePriceEl = document.getElementById('storagePrice');
|
|
this.storageAmount = document.getElementById('storageAmount');
|
|
this.totalPrice = document.getElementById('totalPrice');
|
|
|
|
// Order button
|
|
this.orderButton = document.querySelector('a[href="#order-form"]');
|
|
}
|
|
|
|
// Update slider display values (min/max text below sliders)
|
|
updateSliderDisplayValues() {
|
|
// Update CPU slider display
|
|
if (this.cpuRange) {
|
|
const cpuMinDisplay = document.getElementById('cpuMinDisplay');
|
|
const cpuMaxDisplay = document.getElementById('cpuMaxDisplay');
|
|
if (cpuMinDisplay) cpuMinDisplay.textContent = this.cpuRange.min;
|
|
if (cpuMaxDisplay) cpuMaxDisplay.textContent = this.cpuRange.max;
|
|
}
|
|
|
|
// Update Memory slider display
|
|
if (this.memoryRange) {
|
|
const memoryMinDisplay = document.getElementById('memoryMinDisplay');
|
|
const memoryMaxDisplay = document.getElementById('memoryMaxDisplay');
|
|
if (memoryMinDisplay) memoryMinDisplay.textContent = this.memoryRange.min + ' GB';
|
|
if (memoryMaxDisplay) memoryMaxDisplay.textContent = this.memoryRange.max + ' GB';
|
|
}
|
|
|
|
// Update Storage slider display
|
|
if (this.storageRange) {
|
|
const storageMinDisplay = document.getElementById('storageMinDisplay');
|
|
const storageMaxDisplay = document.getElementById('storageMaxDisplay');
|
|
if (storageMinDisplay) storageMinDisplay.textContent = this.storageRange.min + ' GB';
|
|
if (storageMaxDisplay) storageMaxDisplay.textContent = this.storageRange.max + ' GB';
|
|
}
|
|
|
|
// Update Instances slider display
|
|
if (this.instancesRange) {
|
|
const instancesMinDisplay = document.getElementById('instancesMinDisplay');
|
|
const instancesMaxDisplay = document.getElementById('instancesMaxDisplay');
|
|
if (instancesMinDisplay) instancesMinDisplay.textContent = this.instancesRange.min;
|
|
if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.instancesRange.max;
|
|
}
|
|
}
|
|
|
|
// Setup order button click handler
|
|
setupOrderButton() {
|
|
if (this.orderButton) {
|
|
this.orderButton.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.handleOrderClick();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handle order button click
|
|
handleOrderClick() {
|
|
if (this.selectedConfiguration) {
|
|
// Pre-fill the contact form with configuration details
|
|
this.prefillContactForm();
|
|
|
|
// Scroll to the contact form
|
|
const contactForm = document.getElementById('order-form');
|
|
if (contactForm) {
|
|
contactForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pre-fill contact form with selected configuration
|
|
prefillContactForm() {
|
|
if (!this.selectedConfiguration) return;
|
|
|
|
const config = this.selectedConfiguration;
|
|
|
|
// Create configuration summary message
|
|
const configMessage = this.generateConfigurationMessage(config);
|
|
|
|
// Find and fill the message textarea in the contact form
|
|
const messageField = document.querySelector('#order-form textarea[name="message"]');
|
|
if (messageField) {
|
|
messageField.value = configMessage;
|
|
}
|
|
|
|
// Store configuration details in hidden field
|
|
const detailsField = document.querySelector('#order-form input[name="details"]');
|
|
if (detailsField) {
|
|
detailsField.value = JSON.stringify({
|
|
plan: config.planName,
|
|
vcpus: config.vcpus,
|
|
memory: config.memory,
|
|
storage: config.storage,
|
|
instances: config.instances,
|
|
serviceLevel: config.serviceLevel,
|
|
totalPrice: config.totalPrice
|
|
});
|
|
}
|
|
}
|
|
|
|
// Generate human-readable configuration message
|
|
generateConfigurationMessage(config) {
|
|
return `I would like to order the following configuration:
|
|
|
|
Plan: ${config.planName} (${config.planGroup})
|
|
vCPUs: ${config.vcpus}
|
|
Memory: ${config.memory} GB
|
|
Storage: ${config.storage} GB
|
|
Instances: ${config.instances}
|
|
Service Level: ${config.serviceLevel}
|
|
|
|
Total Monthly Price: CHF ${config.totalPrice}
|
|
|
|
Please contact me with next steps for ordering this configuration.`;
|
|
}
|
|
|
|
// Load pricing data from API endpoint
|
|
async loadPricingData() {
|
|
try {
|
|
const response = await fetch(`/offering/${this.currentOffering.provider_slug}/${this.currentOffering.service_slug}/?pricing=json`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load pricing data');
|
|
}
|
|
|
|
this.pricingData = await response.json();
|
|
|
|
// Extract storage price from the first available plan
|
|
this.extractStoragePrice();
|
|
|
|
this.setupEventListeners();
|
|
this.populatePlanDropdown();
|
|
this.updatePricing();
|
|
} catch (error) {
|
|
console.error('Error loading pricing data:', error);
|
|
this.showError('Failed to load pricing information');
|
|
}
|
|
}
|
|
|
|
// Extract replica information and storage price from pricing data
|
|
extractStoragePrice() {
|
|
if (!this.pricingData) return;
|
|
|
|
// Find the first plan with storage pricing data and replica info
|
|
for (const groupName of Object.keys(this.pricingData)) {
|
|
const group = this.pricingData[groupName];
|
|
for (const serviceLevel of Object.keys(group)) {
|
|
const plans = group[serviceLevel];
|
|
if (plans.length > 0 && plans[0].storage_price !== undefined) {
|
|
this.storagePrice = parseFloat(plans[0].storage_price);
|
|
this.replicaInfo = {
|
|
ha_replica_min: plans[0].ha_replica_min || 1,
|
|
ha_replica_max: plans[0].ha_replica_max || 1
|
|
};
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup event listeners for calculator controls
|
|
setupEventListeners() {
|
|
if (!this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
|
|
|
|
// Setup service levels based on available data
|
|
this.setupServiceLevels();
|
|
|
|
// Slider event listeners
|
|
this.cpuRange.addEventListener('input', () => {
|
|
this.cpuValue.textContent = this.cpuRange.value;
|
|
this.updatePricing();
|
|
});
|
|
|
|
this.memoryRange.addEventListener('input', () => {
|
|
this.memoryValue.textContent = this.memoryRange.value;
|
|
this.updatePricing();
|
|
});
|
|
|
|
this.storageRange.addEventListener('input', () => {
|
|
this.storageValue.textContent = this.storageRange.value;
|
|
this.updatePricing();
|
|
});
|
|
|
|
this.instancesRange.addEventListener('input', () => {
|
|
this.instancesValue.textContent = this.instancesRange.value;
|
|
this.updatePricing();
|
|
});
|
|
|
|
// Service level change listeners
|
|
this.serviceLevelInputs.forEach(input => {
|
|
input.addEventListener('change', () => {
|
|
this.updateInstancesSlider();
|
|
this.populatePlanDropdown();
|
|
this.updatePricing();
|
|
});
|
|
});
|
|
|
|
// Plan selection listener
|
|
if (this.planSelect) {
|
|
this.planSelect.addEventListener('change', () => {
|
|
if (this.planSelect.value) {
|
|
const selectedPlan = JSON.parse(this.planSelect.value);
|
|
|
|
// Update sliders to match selected plan
|
|
this.cpuRange.value = selectedPlan.vcpus;
|
|
this.memoryRange.value = selectedPlan.ram;
|
|
this.cpuValue.textContent = selectedPlan.vcpus;
|
|
this.memoryValue.textContent = selectedPlan.ram;
|
|
|
|
this.updatePricingWithPlan(selectedPlan);
|
|
} else {
|
|
this.updatePricing();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize instances slider
|
|
this.updateInstancesSlider();
|
|
}
|
|
|
|
// Update instances slider based on service level and replica info
|
|
updateInstancesSlider() {
|
|
if (!this.instancesRange || !this.replicaInfo) return;
|
|
|
|
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
|
|
|
|
if (serviceLevel === 'Guaranteed Availability') {
|
|
// For GA, min is ha_replica_min
|
|
this.instancesRange.min = this.replicaInfo.ha_replica_min;
|
|
this.instancesRange.value = Math.max(this.instancesRange.value, this.replicaInfo.ha_replica_min);
|
|
} else {
|
|
// For BE, min is 1
|
|
this.instancesRange.min = 1;
|
|
this.instancesRange.value = Math.max(this.instancesRange.value, 1);
|
|
}
|
|
|
|
// Set max to ha_replica_max
|
|
this.instancesRange.max = this.replicaInfo.ha_replica_max;
|
|
|
|
// Update display value
|
|
this.instancesValue.textContent = this.instancesRange.value;
|
|
|
|
// Update the min/max display under the slider using direct IDs
|
|
const instancesMinDisplay = document.getElementById('instancesMinDisplay');
|
|
const instancesMaxDisplay = document.getElementById('instancesMaxDisplay');
|
|
|
|
if (instancesMinDisplay) instancesMinDisplay.textContent = this.instancesRange.min;
|
|
if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.instancesRange.max;
|
|
}
|
|
|
|
// Setup service levels dynamically from pricing data
|
|
setupServiceLevels() {
|
|
if (!this.pricingData) return;
|
|
|
|
const serviceLevelGroup = document.getElementById('serviceLevelGroup');
|
|
if (!serviceLevelGroup) return;
|
|
|
|
// Get all available service levels from the pricing data
|
|
const availableServiceLevels = new Set();
|
|
Object.keys(this.pricingData).forEach(groupName => {
|
|
const group = this.pricingData[groupName];
|
|
Object.keys(group).forEach(serviceLevel => {
|
|
availableServiceLevels.add(serviceLevel);
|
|
});
|
|
});
|
|
|
|
// Clear existing service level buttons
|
|
serviceLevelGroup.innerHTML = '';
|
|
|
|
// Create buttons for each available service level
|
|
let isFirst = true;
|
|
availableServiceLevels.forEach(serviceLevel => {
|
|
const inputId = `serviceLevel${serviceLevel.replace(/\s+/g, '')}`;
|
|
|
|
// Create radio input
|
|
const input = document.createElement('input');
|
|
input.type = 'radio';
|
|
input.className = 'btn-check';
|
|
input.name = 'serviceLevel';
|
|
input.id = inputId;
|
|
input.value = serviceLevel;
|
|
if (isFirst) {
|
|
input.checked = true;
|
|
isFirst = false;
|
|
}
|
|
|
|
// Create label
|
|
const label = document.createElement('label');
|
|
label.className = 'btn btn-outline-primary';
|
|
label.setAttribute('for', inputId);
|
|
label.textContent = serviceLevel;
|
|
|
|
// Add event listener
|
|
input.addEventListener('change', () => {
|
|
this.updateInstancesSlider();
|
|
this.populatePlanDropdown();
|
|
this.updatePricing();
|
|
});
|
|
|
|
serviceLevelGroup.appendChild(input);
|
|
serviceLevelGroup.appendChild(label);
|
|
});
|
|
|
|
// Update the serviceLevelInputs reference
|
|
this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
|
|
|
|
// Calculate and set slider maximums based on available plans - this will call updateSliderDisplayValues()
|
|
this.updateSliderMaximums();
|
|
}
|
|
|
|
// Calculate maximum values for sliders based on available plans
|
|
updateSliderMaximums() {
|
|
if (!this.pricingData || !this.cpuRange || !this.memoryRange) return;
|
|
|
|
let maxCpus = 0;
|
|
let maxMemory = 0;
|
|
|
|
// Find maximum CPU and memory across all plans
|
|
Object.keys(this.pricingData).forEach(groupName => {
|
|
const group = this.pricingData[groupName];
|
|
Object.keys(group).forEach(serviceLevel => {
|
|
group[serviceLevel].forEach(plan => {
|
|
const planCpus = parseFloat(plan.vcpus);
|
|
const planMemory = parseFloat(plan.ram);
|
|
|
|
if (planCpus > maxCpus) maxCpus = planCpus;
|
|
if (planMemory > maxMemory) maxMemory = planMemory;
|
|
});
|
|
});
|
|
});
|
|
|
|
// Set slider maximums with some padding
|
|
if (maxCpus > 0) {
|
|
this.cpuRange.max = Math.ceil(maxCpus);
|
|
}
|
|
|
|
if (maxMemory > 0) {
|
|
this.memoryRange.max = Math.ceil(maxMemory);
|
|
}
|
|
|
|
// Update display values after changing min/max - moved to end and call explicitly
|
|
this.updateSliderDisplayValues();
|
|
}
|
|
|
|
// Populate plan dropdown based on selected service level
|
|
populatePlanDropdown() {
|
|
if (!this.planSelect || !this.pricingData) return;
|
|
|
|
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
|
|
if (!serviceLevel) return;
|
|
|
|
// Clear existing options
|
|
this.planSelect.innerHTML = '<option value="">Auto-select best matching plan</option>';
|
|
|
|
// Collect all plans for selected service level
|
|
const availablePlans = [];
|
|
Object.keys(this.pricingData).forEach(groupName => {
|
|
const group = this.pricingData[groupName];
|
|
if (group[serviceLevel]) {
|
|
group[serviceLevel].forEach(plan => {
|
|
availablePlans.push({
|
|
...plan,
|
|
groupName: groupName
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// Sort plans by vCPU, then by RAM
|
|
availablePlans.sort((a, b) => {
|
|
if (parseInt(a.vcpus) !== parseInt(b.vcpus)) {
|
|
return parseInt(a.vcpus) - parseInt(b.vcpus);
|
|
}
|
|
return parseInt(a.ram) - parseInt(b.ram);
|
|
});
|
|
|
|
// Add plans to dropdown
|
|
availablePlans.forEach(plan => {
|
|
const option = document.createElement('option');
|
|
option.value = JSON.stringify(plan);
|
|
option.textContent = `${plan.compute_plan} - ${plan.vcpus} vCPUs, ${plan.ram} GB RAM`;
|
|
this.planSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// Find best matching plan based on requirements
|
|
findBestMatchingPlan(cpus, memory, serviceLevel) {
|
|
if (!this.pricingData) return null;
|
|
|
|
let bestMatch = null;
|
|
let bestScore = Infinity;
|
|
|
|
// Iterate through all groups and service levels
|
|
Object.keys(this.pricingData).forEach(groupName => {
|
|
const group = this.pricingData[groupName];
|
|
|
|
if (group[serviceLevel]) {
|
|
group[serviceLevel].forEach(plan => {
|
|
const planCpus = parseInt(plan.vcpus);
|
|
const planMemory = parseInt(plan.ram);
|
|
|
|
// Check if plan meets minimum requirements
|
|
if (planCpus >= cpus && planMemory >= memory) {
|
|
// Calculate efficiency score (lower is better)
|
|
const cpuOverhead = planCpus - cpus;
|
|
const memoryOverhead = planMemory - memory;
|
|
const score = cpuOverhead + memoryOverhead + plan.final_price * 0.1;
|
|
|
|
if (score < bestScore) {
|
|
bestScore = score;
|
|
bestMatch = {
|
|
...plan,
|
|
groupName: groupName
|
|
};
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return bestMatch;
|
|
}
|
|
|
|
// Update pricing with specific plan
|
|
updatePricingWithPlan(selectedPlan) {
|
|
const storage = parseInt(this.storageRange?.value || 20);
|
|
const instances = parseInt(this.instancesRange?.value || 1);
|
|
|
|
this.showPlanDetails(selectedPlan, storage, instances);
|
|
this.updateStatusMessage('Plan selected directly!', 'success');
|
|
}
|
|
|
|
// Main pricing update function
|
|
updatePricing() {
|
|
if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
|
|
|
|
// Reset plan selection if in auto-select mode
|
|
if (!this.planSelect?.value) {
|
|
const cpus = parseInt(this.cpuRange.value);
|
|
const memory = parseInt(this.memoryRange.value);
|
|
const storage = parseInt(this.storageRange.value);
|
|
const instances = parseInt(this.instancesRange.value);
|
|
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
|
|
|
|
if (!serviceLevel) return;
|
|
|
|
// Find best matching plan
|
|
const matchedPlan = this.findBestMatchingPlan(cpus, memory, serviceLevel);
|
|
|
|
if (matchedPlan) {
|
|
this.showPlanDetails(matchedPlan, storage, instances);
|
|
this.updateStatusMessage('Perfect match found!', 'success');
|
|
} else {
|
|
this.showNoMatch();
|
|
}
|
|
} else {
|
|
// Plan is directly selected, update storage pricing
|
|
const selectedPlan = JSON.parse(this.planSelect.value);
|
|
this.updatePricingWithPlan(selectedPlan);
|
|
}
|
|
}
|
|
|
|
// Show plan details in the UI
|
|
showPlanDetails(plan, storage, instances) {
|
|
if (!this.selectedPlanDetails) return;
|
|
|
|
// Show plan details section
|
|
this.planMatchStatus.style.display = 'block';
|
|
this.selectedPlanDetails.style.display = 'block';
|
|
if (this.noMatchFound) this.noMatchFound.style.display = 'none';
|
|
|
|
// Get current service level
|
|
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value || 'Best Effort';
|
|
|
|
// Update plan information
|
|
if (this.planGroup) this.planGroup.textContent = plan.groupName;
|
|
if (this.planName) this.planName.textContent = plan.compute_plan;
|
|
if (this.planDescription) this.planDescription.textContent = plan.compute_plan_group_description || '';
|
|
if (this.planCpus) this.planCpus.textContent = plan.vcpus;
|
|
if (this.planMemory) this.planMemory.textContent = plan.ram + ' GB';
|
|
if (this.planInstances) this.planInstances.textContent = instances;
|
|
if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel;
|
|
|
|
// Calculate pricing using storage price from the plan data
|
|
const computePriceValue = parseFloat(plan.compute_plan_price);
|
|
const servicePriceValue = parseFloat(plan.sla_price);
|
|
const managedServicePricePerInstance = computePriceValue + servicePriceValue;
|
|
const managedServicePrice = managedServicePricePerInstance * instances;
|
|
|
|
// Use storage price from plan data or fallback to instance variable
|
|
const storageUnitPrice = plan.storage_price !== undefined ? parseFloat(plan.storage_price) : this.storagePrice;
|
|
const storagePriceValue = storage * storageUnitPrice * instances;
|
|
const totalPriceValue = managedServicePrice + storagePriceValue;
|
|
|
|
// Update pricing display
|
|
if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2);
|
|
if (this.storagePriceEl) this.storagePriceEl.textContent = storagePriceValue.toFixed(2);
|
|
if (this.storageAmount) this.storageAmount.textContent = storage;
|
|
if (this.totalPrice) this.totalPrice.textContent = totalPriceValue.toFixed(2);
|
|
|
|
// Store current configuration for order button
|
|
this.selectedConfiguration = {
|
|
planName: plan.compute_plan,
|
|
planGroup: plan.groupName,
|
|
vcpus: plan.vcpus,
|
|
memory: plan.ram,
|
|
storage: storage,
|
|
instances: instances,
|
|
serviceLevel: serviceLevel,
|
|
totalPrice: totalPriceValue.toFixed(2)
|
|
};
|
|
}
|
|
|
|
// Show no matching plan found
|
|
showNoMatch() {
|
|
if (this.planMatchStatus) this.planMatchStatus.style.display = 'none';
|
|
if (this.selectedPlanDetails) this.selectedPlanDetails.style.display = 'none';
|
|
if (this.noMatchFound) this.noMatchFound.style.display = 'block';
|
|
}
|
|
|
|
// Update status message
|
|
updateStatusMessage(message, type) {
|
|
if (!this.planMatchStatus) return;
|
|
|
|
const iconClass = type === 'success' ? 'bi-check-circle' : 'bi-info-circle';
|
|
const textClass = type === 'success' ? 'text-success' : '';
|
|
const alertClass = type === 'success' ? 'alert-success' : 'alert-info';
|
|
|
|
this.planMatchStatus.innerHTML = `<i class="bi ${iconClass} me-2 ${textClass}"></i><span class="${textClass}">${message}</span>`;
|
|
this.planMatchStatus.className = `alert ${alertClass} mb-3`;
|
|
this.planMatchStatus.style.display = 'block';
|
|
}
|
|
|
|
// Show error message
|
|
showError(message) {
|
|
if (this.planMatchStatus) {
|
|
this.planMatchStatus.innerHTML = `<i class="bi bi-exclamation-triangle me-2 text-danger"></i><span class="text-danger">${message}</span>`;
|
|
this.planMatchStatus.className = 'alert alert-danger mb-3';
|
|
this.planMatchStatus.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize calculator when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Only initialize if we're on an offering detail page with pricing calculator
|
|
if (document.getElementById('cpuRange')) {
|
|
new PriceCalculator();
|
|
}
|
|
}); |