refactor price calculator into multiple files
This commit is contained in:
parent
33e8f2152a
commit
67e1b4cab1
8 changed files with 1324 additions and 1877 deletions
File diff suppressed because it is too large
Load diff
176
hub/services/static/js/price-calculator/addon-manager.js
Normal file
176
hub/services/static/js/price-calculator/addon-manager.js
Normal file
|
@ -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 = `
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input addon-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
id="addon-${addon.id}"
|
||||||
|
value="${addon.id}"
|
||||||
|
data-addon='${JSON.stringify(addon)}'
|
||||||
|
${addon.is_mandatory ? 'checked disabled' : ''}>
|
||||||
|
<label class="form-check-label" for="addon-${addon.id}">
|
||||||
|
<strong>${addon.name}</strong>
|
||||||
|
<div class="text-muted small">${addon.commercial_description || ''}</div>
|
||||||
|
<div class="text-primary addon-price-display">
|
||||||
|
${addon.is_mandatory ? 'Required - ' : ''}CHF <span class="addon-price-value">0.00</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
160
hub/services/static/js/price-calculator/dom-manager.js
Normal file
160
hub/services/static/js/price-calculator/dom-manager.js
Normal file
|
@ -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;
|
113
hub/services/static/js/price-calculator/order-manager.js
Normal file
113
hub/services/static/js/price-calculator/order-manager.js
Normal file
|
@ -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;
|
104
hub/services/static/js/price-calculator/plan-manager.js
Normal file
104
hub/services/static/js/price-calculator/plan-manager.js
Normal file
|
@ -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 = '<option value="">Auto-select best matching plan</option>';
|
||||||
|
|
||||||
|
// 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;
|
252
hub/services/static/js/price-calculator/price-calculator.js
Normal file
252
hub/services/static/js/price-calculator/price-calculator.js
Normal file
|
@ -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;
|
190
hub/services/static/js/price-calculator/pricing-data-manager.js
Normal file
190
hub/services/static/js/price-calculator/pricing-data-manager.js
Normal file
|
@ -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;
|
269
hub/services/static/js/price-calculator/ui-manager.js
Normal file
269
hub/services/static/js/price-calculator/ui-manager.js
Normal file
|
@ -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 = `<i class="bi ${iconClass} me-2 ${textClass}"></i><span class="${textClass}">${message}</span>`;
|
||||||
|
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 = `<i class="bi bi-exclamation-triangle me-2 text-danger"></i><span class="text-danger">${message}</span>`;
|
||||||
|
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 = `
|
||||||
|
<span><i class="bi bi-check-circle text-success me-1"></i>${addon.name}</span>
|
||||||
|
<span>CHF ${addon.price}</span>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<span>Add-on: ${addon.name}</span>
|
||||||
|
<span class="fw-bold">CHF ${addon.price}</span>
|
||||||
|
`;
|
||||||
|
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;
|
Loading…
Add table
Add a link
Reference in a new issue