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