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

330 lines
14 KiB
JavaScript

/**
* 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) {
// Get references to the managed service includes elements
const managedServiceIncludesContainer = domManager.get('managedServiceIncludesContainer');
if (managedServiceIncludesContainer) {
// Clear existing content
managedServiceIncludesContainer.innerHTML = '';
// Always add "Compute" as the first item
const computeRow = document.createElement('div');
computeRow.className = 'd-flex justify-content-between small text-muted mb-1';
computeRow.innerHTML = `
<span>Compute (vCPUs & Memory)</span>
<span>Included</span>
`;
managedServiceIncludesContainer.appendChild(computeRow);
// 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>Add-on: ${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 align-items-center mb-2';
addonRow.innerHTML = `
<span class="text-nowrap flex-shrink-1" style="min-width: 0;">Add-on: ${addon.name}</span>
<span class="fw-bold text-nowrap flex-shrink-0" style="min-width: 110px; text-align: right;">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"]');
// Note: Event listeners are now handled in price-calculator.js setupEventListeners method
// to properly preserve plan selection when service level changes
// this.setupServiceLevelEventListeners(domManager, pricingDataManager);
}
// Setup event listeners for service level inputs
setupServiceLevelEventListeners(domManager, pricingDataManager) {
const serviceLevelInputs = domManager.get('serviceLevelInputs');
if (!serviceLevelInputs) return;
// Get the main price calculator instance from window
const priceCalculator = window.priceCalculator;
if (!priceCalculator) return;
serviceLevelInputs.forEach(input => {
input.addEventListener('change', () => {
this.updateInstancesSlider(domManager, pricingDataManager);
priceCalculator.planManager.populatePlanDropdown(domManager);
priceCalculator.addonManager.updateAddons(domManager);
priceCalculator.updatePricing();
});
});
}
// 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 { cpuValues, memoryValues } = pricingDataManager.getAvailableSliderValues();
// Set CPU slider range based on available plan values
if (cpuValues.length > 0) {
cpuRange.min = Math.min(...cpuValues);
cpuRange.max = Math.max(...cpuValues);
// Calculate step size - use the smallest difference between consecutive values
const cpuStep = this.calculateOptimalStep(cpuValues);
cpuRange.step = cpuStep;
}
// Set Memory slider range based on available plan values
if (memoryValues.length > 0) {
memoryRange.min = Math.min(...memoryValues);
memoryRange.max = Math.max(...memoryValues);
// Calculate step size - use the smallest difference between consecutive values
const memoryStep = this.calculateOptimalStep(memoryValues);
memoryRange.step = memoryStep;
}
// Update display values after changing min/max
domManager.updateSliderDisplayValues();
}
// Calculate optimal step size for slider based on available values
calculateOptimalStep(values) {
if (values.length <= 1) return 0.25; // Default step
// Find the smallest difference between consecutive values
let minDiff = Infinity;
for (let i = 1; i < values.length; i++) {
const diff = values[i] - values[i - 1];
if (diff > 0 && diff < minDiff) {
minDiff = diff;
}
}
// Use the minimum difference as step, but ensure it's reasonable
// Round to common step values (0.25, 0.5, 1, etc.)
if (minDiff <= 0.25) return 0.25;
if (minDiff <= 0.5) return 0.5;
if (minDiff <= 1) return 1;
return Math.ceil(minDiff);
}
// 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;