website/hub/services/static/js/price-calculator/price-calculator.js

438 lines
17 KiB
JavaScript

/**
* Price Calculator - Main orchestrator class
* Coordinates all the different managers to provide pricing calculation functionality
*/
class PriceCalculator {
constructor() {
try {
// Initialize managers
this.domManager = new DOMManager();
this.currentOffering = this.extractOfferingFromURL();
if (!this.currentOffering) {
throw new Error('Unable to extract offering information from URL');
}
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();
} catch (error) {
console.error('Error initializing PriceCalculator:', error);
this.showInitializationError(error.message);
}
}
// 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 {
throw new Error('No current offering found');
}
} catch (error) {
console.error('Error initializing price calculator:', error);
this.showInitializationError(error.message);
}
}
// Show initialization error to user
showInitializationError(message) {
const planMatchStatus = this.domManager?.get('planMatchStatus');
if (planMatchStatus) {
planMatchStatus.innerHTML = `
<i class="bi bi-exclamation-triangle me-2 text-danger"></i>
<span class="text-danger">Failed to load pricing calculator: ${message}</span>
`;
planMatchStatus.className = 'alert alert-danger mb-3';
planMatchStatus.style.display = 'block';
}
}
// Setup initial UI components
setupUI() {
// Setup service levels based on available data
this.uiManager.setupServiceLevels(this.domManager, this.pricingDataManager);
// Calculate and set slider maximums and ranges
this.uiManager.updateSliderMaximums(this.domManager, this.pricingDataManager);
// Set smart default values based on available plans
this.domManager.setSmartDefaults(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;
// Only synchronize if in auto-select mode (no manual plan selection)
const planSelect = this.domManager.get('planSelect');
if (!planSelect?.value) {
this.synchronizeMemoryToMatchingPlan(parseFloat(cpuRange.value));
}
this.updatePricing();
});
memoryRange.addEventListener('input', () => {
this.domManager.get('memoryValue').textContent = memoryRange.value;
// Only synchronize if in auto-select mode (no manual plan selection)
const planSelect = this.domManager.get('planSelect');
if (!planSelect?.value) {
this.synchronizeCpuToMatchingPlan(parseFloat(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();
});
// 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 smart default values
this.domManager.setSmartDefaults(this.pricingDataManager);
// 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();
});
// Service level change listener
const serviceLevelInputs = this.domManager.get('serviceLevelInputs');
if (serviceLevelInputs) {
serviceLevelInputs.forEach(input => {
input.addEventListener('change', () => {
// Update plan dropdown for new service level
this.planManager.populatePlanDropdown(this.domManager);
// Update addons for new service level
this.addonManager.updateAddons(this.domManager);
// Update pricing
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]
);
}
// Synchronize memory slider to match CPU value with best matching plan
synchronizeMemoryToMatchingPlan(targetCpu) {
const serviceLevel = this.domManager.getSelectedServiceLevel();
if (!serviceLevel) return;
// Get all available plans for the current service level
const availablePlans = this.pricingDataManager.getPlansForServiceLevel(serviceLevel);
if (!availablePlans || availablePlans.length === 0) return;
// Snap CPU to nearest available value first
const { cpuValues } = this.pricingDataManager.getAvailableSliderValues();
const snappedCpu = this.findNearestValue(targetCpu, cpuValues);
// Update CPU slider to snapped value if different
if (snappedCpu !== targetCpu) {
const cpuRange = this.domManager.get('cpuRange');
const cpuValue = this.domManager.get('cpuValue');
if (cpuRange && cpuValue) {
cpuRange.value = snappedCpu;
cpuValue.textContent = snappedCpu;
}
}
// Find the plan that best matches the snapped CPU requirement
let bestPlan = null;
let minDifference = Infinity;
availablePlans.forEach(plan => {
const planCpu = parseFloat(plan.vcpus);
// Look for plans that meet or exceed the CPU requirement
if (planCpu >= snappedCpu) {
const difference = planCpu - snappedCpu;
if (difference < minDifference) {
minDifference = difference;
bestPlan = plan;
}
}
});
// If no plan meets the CPU requirement, find the closest one below it
if (!bestPlan) {
availablePlans.forEach(plan => {
const planCpu = parseFloat(plan.vcpus);
const difference = Math.abs(planCpu - snappedCpu);
if (difference < minDifference) {
minDifference = difference;
bestPlan = plan;
}
});
}
// Update memory slider to match the found plan
if (bestPlan) {
const memoryRange = this.domManager.get('memoryRange');
const memoryValue = this.domManager.get('memoryValue');
if (memoryRange && memoryValue) {
memoryRange.value = bestPlan.ram;
memoryValue.textContent = bestPlan.ram;
}
}
}
// Synchronize CPU slider to match memory value with best matching plan
synchronizeCpuToMatchingPlan(targetMemory) {
const serviceLevel = this.domManager.getSelectedServiceLevel();
if (!serviceLevel) return;
// Get all available plans for the current service level
const availablePlans = this.pricingDataManager.getPlansForServiceLevel(serviceLevel);
if (!availablePlans || availablePlans.length === 0) return;
// Snap memory to nearest available value first
const { memoryValues } = this.pricingDataManager.getAvailableSliderValues();
const snappedMemory = this.findNearestValue(targetMemory, memoryValues);
// Update memory slider to snapped value if different
if (snappedMemory !== targetMemory) {
const memoryRange = this.domManager.get('memoryRange');
const memoryValue = this.domManager.get('memoryValue');
if (memoryRange && memoryValue) {
memoryRange.value = snappedMemory;
memoryValue.textContent = snappedMemory;
}
}
// Find the plan that best matches the snapped memory requirement
let bestPlan = null;
let minDifference = Infinity;
availablePlans.forEach(plan => {
const planMemory = parseFloat(plan.ram);
// Look for plans that meet or exceed the memory requirement
if (planMemory >= snappedMemory) {
const difference = planMemory - snappedMemory;
if (difference < minDifference) {
minDifference = difference;
bestPlan = plan;
}
}
});
// If no plan meets the memory requirement, find the closest one below it
if (!bestPlan) {
availablePlans.forEach(plan => {
const planMemory = parseFloat(plan.ram);
const difference = Math.abs(planMemory - snappedMemory);
if (difference < minDifference) {
minDifference = difference;
bestPlan = plan;
}
});
}
// Update CPU slider to match the found plan
if (bestPlan) {
const cpuRange = this.domManager.get('cpuRange');
const cpuValue = this.domManager.get('cpuValue');
if (cpuRange && cpuValue) {
cpuRange.value = bestPlan.vcpus;
cpuValue.textContent = bestPlan.vcpus;
}
}
}
// Find the nearest value in an array to a target value
findNearestValue(target, availableValues) {
if (!availableValues || availableValues.length === 0) return target;
let nearest = availableValues[0];
let minDifference = Math.abs(target - nearest);
for (let i = 1; i < availableValues.length; i++) {
const difference = Math.abs(target - availableValues[i]);
if (difference < minDifference) {
minDifference = difference;
nearest = availableValues[i];
}
}
return nearest;
}
}
// Export for use in other modules
window.PriceCalculator = PriceCalculator;