still buggy price calculator

This commit is contained in:
Tobias Brunner 2025-06-19 17:05:49 +02:00
parent 22e527bcd9
commit 9d423ce61e
No known key found for this signature in database
3 changed files with 268 additions and 350 deletions

View file

@ -271,7 +271,7 @@
<input type="radio" class="btn-check" name="serviceLevel" id="serviceLevelBestEffort" value="Best Effort" checked>
<label class="btn btn-outline-primary" for="serviceLevelBestEffort">Best Effort</label>
<input type="radio" class="btn-check" name="serviceLevel" id="serviceLevelGuaranteed" value="Guaranteed">
<input type="radio" class="btn-check" name="serviceLevel" id="serviceLevelGuaranteed" value="Guaranteed Availability">
<label class="btn btn-outline-primary" for="serviceLevelGuaranteed">Guaranteed Availability</label>
</div>
</div>
@ -460,342 +460,4 @@
</div>
</div>
</section>
<!-- JavaScript for the price calculator with addons support -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Only run this script when the price calculator is present
if (!document.getElementById('cpuRange')) {
return;
}
// Fetch pricing data from the server
fetch(window.location.href + '?pricing=json')
.then(response => response.json())
.then(pricingData => {
initializePriceCalculator(pricingData);
})
.catch(error => {
console.error('Error fetching pricing data:', error);
});
function initializePriceCalculator(pricingData) {
// Store selected addons
let selectedAddons = [];
// UI Controls
const cpuRange = document.getElementById('cpuRange');
const memoryRange = document.getElementById('memoryRange');
const storageRange = document.getElementById('storageRange');
const instancesRange = document.getElementById('instancesRange');
const serviceLevelGroup = document.getElementById('serviceLevelGroup');
const planSelect = document.getElementById('planSelect');
const addonsContainer = document.getElementById('addonsContainer');
const addonPricingContainer = document.getElementById('addonPricingContainer');
// Results UI elements
const planMatchStatus = document.getElementById('planMatchStatus');
const selectedPlanDetails = document.getElementById('selectedPlanDetails');
const noMatchFound = document.getElementById('noMatchFound');
const planName = document.getElementById('planName');
const planGroup = document.getElementById('planGroup');
const planDescription = document.getElementById('planDescription');
const planCpus = document.getElementById('planCpus');
const planMemory = document.getElementById('planMemory');
const planInstances = document.getElementById('planInstances');
const planServiceLevel = document.getElementById('planServiceLevel');
const managedServicePrice = document.getElementById('managedServicePrice');
const storagePrice = document.getElementById('storagePrice');
const storageAmount = document.getElementById('storageAmount');
const totalPrice = document.getElementById('totalPrice');
// Find all plan options for the select dropdown
populatePlanOptions(pricingData);
// Populate optional addons
populateAddons(pricingData);
// Set up event listeners
cpuRange.addEventListener('input', updateCalculator);
memoryRange.addEventListener('input', updateCalculator);
storageRange.addEventListener('input', updateCalculator);
instancesRange.addEventListener('input', updateCalculator);
planSelect.addEventListener('change', handlePlanSelection);
// Add listeners for service level radio buttons
const serviceLevelRadios = document.querySelectorAll('input[name="serviceLevel"]');
serviceLevelRadios.forEach(radio => {
radio.addEventListener('change', updateCalculator);
});
// Initial calculation
updateCalculator();
function populatePlanOptions(pricingData) {
const planOption = document.createElement('option');
planOption.value = '';
planOption.textContent = 'Auto-select best matching plan';
planSelect.appendChild(planOption);
// Add all available plans to the dropdown
Object.keys(pricingData).forEach(groupName => {
const group = pricingData[groupName];
// Create optgroup for the plan group
const optgroup = document.createElement('optgroup');
optgroup.label = groupName;
// Add plans from each service level
Object.keys(group).forEach(serviceLevel => {
const plans = group[serviceLevel];
plans.forEach(plan => {
const option = document.createElement('option');
option.value = `${plan.compute_plan}|${serviceLevel}`;
option.textContent = `${plan.compute_plan} (${serviceLevel}) - ${plan.vcpus} vCPU, ${plan.ram} GB RAM`;
option.dataset.planData = JSON.stringify(plan);
optgroup.appendChild(option);
});
});
planSelect.appendChild(optgroup);
});
}
function populateAddons(pricingData) {
// Get a sample plan to extract addons (assuming all plans have the same addons)
let samplePlan = null;
for (const groupName in pricingData) {
for (const serviceLevel in pricingData[groupName]) {
if (pricingData[groupName][serviceLevel].length > 0) {
samplePlan = pricingData[groupName][serviceLevel][0];
break;
}
}
if (samplePlan) break;
}
if (!samplePlan || !samplePlan.optional_addons) {
addonsContainer.innerHTML = '<p class="text-muted">No optional add-ons available</p>';
return;
}
// Create UI for each optional addon
samplePlan.optional_addons.forEach(addon => {
const addonDiv = document.createElement('div');
addonDiv.className = 'form-check mb-2';
const addonCheckbox = document.createElement('input');
addonCheckbox.type = 'checkbox';
addonCheckbox.className = 'form-check-input addon-checkbox';
addonCheckbox.id = `addon-${addon.id}`;
addonCheckbox.dataset.addonId = addon.id;
addonCheckbox.addEventListener('change', function() {
if (this.checked) {
selectedAddons.push(addon.id);
} else {
selectedAddons = selectedAddons.filter(id => id !== addon.id);
}
updateCalculator();
});
const addonLabel = document.createElement('label');
addonLabel.className = 'form-check-label';
addonLabel.htmlFor = `addon-${addon.id}`;
addonLabel.innerHTML = `${addon.name} <small class="text-muted">${addon.commercial_description || addon.description || ''}</small>`;
addonDiv.appendChild(addonCheckbox);
addonDiv.appendChild(addonLabel);
addonsContainer.appendChild(addonDiv);
});
if (samplePlan.optional_addons.length === 0) {
addonsContainer.innerHTML = '<p class="text-muted">No optional add-ons available</p>';
}
}
function updateCalculator() {
// If a specific plan is selected, use that
if (planSelect.value) {
handlePlanSelection();
return;
}
// Get current values from UI
const cpuValue = parseInt(cpuRange.value);
const memoryValue = parseInt(memoryRange.value);
const storageValue = parseInt(storageRange.value);
const instancesValue = parseInt(instancesRange.value);
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked').value;
// Find the best matching plan
const bestPlan = findBestMatchingPlan(cpuValue, memoryValue, serviceLevel);
if (bestPlan) {
displayPlanDetails(bestPlan, storageValue, serviceLevel);
} else {
// No matching plan found
planMatchStatus.style.display = 'none';
selectedPlanDetails.style.display = 'none';
noMatchFound.style.display = 'block';
}
}
function handlePlanSelection() {
if (!planSelect.value) {
updateCalculator();
return;
}
const selectedOption = planSelect.options[planSelect.selectedIndex];
const planData = JSON.parse(selectedOption.dataset.planData);
// Get current values from UI
const storageValue = parseInt(storageRange.value);
const serviceLevel = planData.service_level;
// Update service level radio buttons
document.querySelectorAll('input[name="serviceLevel"]').forEach(radio => {
if (radio.value === serviceLevel) {
radio.checked = true;
}
});
// Update sliders to match selected plan
cpuRange.value = planData.vcpus;
document.getElementById('cpuValue').textContent = planData.vcpus;
memoryRange.value = planData.ram;
document.getElementById('memoryValue').textContent = planData.ram;
displayPlanDetails(planData, storageValue, serviceLevel);
}
function findBestMatchingPlan(cpuValue, memoryValue, serviceLevel) {
let bestMatch = null;
let bestMatchScore = Number.MAX_SAFE_INTEGER;
// Search through all groups and plans
Object.keys(pricingData).forEach(groupName => {
const group = pricingData[groupName];
if (group[serviceLevel]) {
group[serviceLevel].forEach(plan => {
// Calculate how well this plan matches the requirements
const cpuDiff = Math.abs(plan.vcpus - cpuValue);
const memoryDiff = Math.abs(plan.ram - memoryValue);
// Simple scoring: sum of differences, lower is better
const score = cpuDiff + memoryDiff;
// Check if this plan meets minimum requirements
if (plan.vcpus >= cpuValue && plan.ram >= memoryValue) {
// If this is a better match than the current best
if (score < bestMatchScore) {
bestMatch = plan;
bestMatchScore = score;
}
}
});
}
});
return bestMatch;
}
function displayPlanDetails(plan, storageValue, serviceLevel) {
// Update UI to show selected plan
planMatchStatus.style.display = 'none';
selectedPlanDetails.style.display = 'block';
noMatchFound.style.display = 'none';
// Set plan details
planName.textContent = plan.compute_plan;
planGroup.textContent = plan.compute_plan_group;
planDescription.textContent = plan.compute_plan_group_description || '';
planCpus.textContent = plan.vcpus;
planMemory.textContent = plan.ram + ' GB';
planInstances.textContent = '1'; // Default to 1 instance
planServiceLevel.textContent = serviceLevel;
// Calculate prices
const storageCost = (storageValue * plan.storage_price).toFixed(2);
const totalMonthlyCost = (plan.final_price + parseFloat(storageCost)).toFixed(2);
// Update price displays
managedServicePrice.textContent = plan.final_price.toFixed(2);
storagePrice.textContent = storageCost;
storageAmount.textContent = storageValue;
// Process addons
updateAddonPricing(plan, serviceLevel);
// Update total price after addons are processed
calculateTotalPrice(plan, storageCost);
}
function updateAddonPricing(plan, serviceLevel) {
// Clear previous addon pricing
addonPricingContainer.innerHTML = '';
// Display mandatory addons first
if (plan.mandatory_addons && plan.mandatory_addons.length > 0) {
plan.mandatory_addons.forEach(addon => {
if (addon.price) {
const addonDiv = document.createElement('div');
addonDiv.className = 'd-flex justify-content-between mb-2';
addonDiv.innerHTML = `
<span>${addon.name} <small class="text-muted">(Required)</small></span>
<span class="fw-bold">CHF <span class="addon-price">${addon.price.toFixed(2)}</span></span>
`;
addonPricingContainer.appendChild(addonDiv);
}
});
}
// Display selected optional addons
if (plan.optional_addons && plan.optional_addons.length > 0) {
plan.optional_addons.forEach(addon => {
if (selectedAddons.includes(addon.id) && addon.price) {
const addonDiv = document.createElement('div');
addonDiv.className = 'd-flex justify-content-between mb-2';
addonDiv.innerHTML = `
<span>${addon.name}</span>
<span class="fw-bold">CHF <span class="addon-price">${addon.price.toFixed(2)}</span></span>
`;
addonPricingContainer.appendChild(addonDiv);
}
});
}
}
function calculateTotalPrice(plan, storageCost) {
let addonTotal = 0;
// Add mandatory addons
if (plan.mandatory_addons) {
plan.mandatory_addons.forEach(addon => {
if (addon.price) {
addonTotal += addon.price;
}
});
}
// Add selected optional addons
if (plan.optional_addons) {
plan.optional_addons.forEach(addon => {
if (selectedAddons.includes(addon.id) && addon.price) {
addonTotal += addon.price;
}
});
}
// Calculate final price
const finalPrice = plan.final_price + parseFloat(storageCost) + addonTotal;
totalPrice.textContent = finalPrice.toFixed(2);
}
}
});
</script>
{% endblock %}