still buggy price calculator
This commit is contained in:
parent
22e527bcd9
commit
9d423ce61e
3 changed files with 268 additions and 350 deletions
|
@ -10,6 +10,7 @@ class PriceCalculator {
|
||||||
this.currentOffering = null;
|
this.currentOffering = null;
|
||||||
this.selectedConfiguration = null;
|
this.selectedConfiguration = null;
|
||||||
this.replicaInfo = null;
|
this.replicaInfo = null;
|
||||||
|
this.addonsData = null;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +51,10 @@ class PriceCalculator {
|
||||||
this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
|
this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
|
||||||
this.planSelect = document.getElementById('planSelect');
|
this.planSelect = document.getElementById('planSelect');
|
||||||
|
|
||||||
|
// Addon elements
|
||||||
|
this.addonsContainer = document.getElementById('addonsContainer');
|
||||||
|
this.addonPricingContainer = document.getElementById('addonPricingContainer');
|
||||||
|
|
||||||
// Result display elements
|
// Result display elements
|
||||||
this.planMatchStatus = document.getElementById('planMatchStatus');
|
this.planMatchStatus = document.getElementById('planMatchStatus');
|
||||||
this.selectedPlanDetails = document.getElementById('selectedPlanDetails');
|
this.selectedPlanDetails = document.getElementById('selectedPlanDetails');
|
||||||
|
@ -156,25 +161,36 @@ class PriceCalculator {
|
||||||
storage: config.storage,
|
storage: config.storage,
|
||||||
instances: config.instances,
|
instances: config.instances,
|
||||||
serviceLevel: config.serviceLevel,
|
serviceLevel: config.serviceLevel,
|
||||||
totalPrice: config.totalPrice
|
totalPrice: config.totalPrice,
|
||||||
|
addons: config.addons || []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate human-readable configuration message
|
// Generate human-readable configuration message
|
||||||
generateConfigurationMessage(config) {
|
generateConfigurationMessage(config) {
|
||||||
return `I would like to order the following configuration:
|
let message = `I would like to order the following configuration:
|
||||||
|
|
||||||
Plan: ${config.planName} (${config.planGroup})
|
Plan: ${config.planName} (${config.planGroup})
|
||||||
vCPUs: ${config.vcpus}
|
vCPUs: ${config.vcpus}
|
||||||
Memory: ${config.memory} GB
|
Memory: ${config.memory} GB
|
||||||
Storage: ${config.storage} GB
|
Storage: ${config.storage} GB
|
||||||
Instances: ${config.instances}
|
Instances: ${config.instances}
|
||||||
Service Level: ${config.serviceLevel}
|
Service Level: ${config.serviceLevel}`;
|
||||||
|
|
||||||
Total Monthly Price: CHF ${config.totalPrice}
|
// 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.`;
|
Please contact me with next steps for ordering this configuration.`;
|
||||||
|
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load pricing data from API endpoint
|
// Load pricing data from API endpoint
|
||||||
|
@ -185,13 +201,18 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
throw new Error('Failed to load pricing data');
|
throw new Error('Failed to load pricing data');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pricingData = await response.json();
|
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
|
// Extract storage price from the first available plan
|
||||||
this.extractStoragePrice();
|
this.extractStoragePrice();
|
||||||
|
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.populatePlanDropdown();
|
this.populatePlanDropdown();
|
||||||
|
this.updateAddons();
|
||||||
this.updatePricing();
|
this.updatePricing();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading pricing data:', error);
|
console.error('Error loading pricing data:', error);
|
||||||
|
@ -220,6 +241,50 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Setup event listeners for calculator controls
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
if (!this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
|
if (!this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
|
||||||
|
@ -253,6 +318,7 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
this.updateInstancesSlider();
|
this.updateInstancesSlider();
|
||||||
this.populatePlanDropdown();
|
this.populatePlanDropdown();
|
||||||
|
this.updateAddons();
|
||||||
this.updatePricing();
|
this.updatePricing();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -269,8 +335,10 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
this.cpuValue.textContent = selectedPlan.vcpus;
|
this.cpuValue.textContent = selectedPlan.vcpus;
|
||||||
this.memoryValue.textContent = selectedPlan.ram;
|
this.memoryValue.textContent = selectedPlan.ram;
|
||||||
|
|
||||||
|
this.updateAddons();
|
||||||
this.updatePricingWithPlan(selectedPlan);
|
this.updatePricingWithPlan(selectedPlan);
|
||||||
} else {
|
} else {
|
||||||
|
this.updateAddons();
|
||||||
this.updatePricing();
|
this.updatePricing();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -356,6 +424,7 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
this.updateInstancesSlider();
|
this.updateInstancesSlider();
|
||||||
this.populatePlanDropdown();
|
this.populatePlanDropdown();
|
||||||
|
this.updateAddons();
|
||||||
this.updatePricing();
|
this.updatePricing();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -445,6 +514,103 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update addons based on current configuration
|
||||||
|
updateAddons() {
|
||||||
|
if (!this.addonsContainer || !this.addonsData) return;
|
||||||
|
|
||||||
|
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value;
|
||||||
|
if (!serviceLevel || !this.addonsData[serviceLevel]) return;
|
||||||
|
|
||||||
|
const addons = this.addonsData[serviceLevel];
|
||||||
|
|
||||||
|
// Clear existing addons
|
||||||
|
this.addonsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// 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', () => {
|
||||||
|
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
|
||||||
|
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;
|
||||||
|
if (addon.addon_type === 'BASE_FEE') {
|
||||||
|
calculatedPrice = parseFloat(addon.price || 0) * instances;
|
||||||
|
} else if (addon.addon_type === 'UNIT_RATE') {
|
||||||
|
calculatedPrice = parseFloat(addon.price_per_unit || 0) * totalUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priceElement) {
|
||||||
|
priceElement.textContent = calculatedPrice.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the checkbox data for later calculation
|
||||||
|
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
|
// Find best matching plan based on requirements
|
||||||
findBestMatchingPlan(cpus, memory, serviceLevel) {
|
findBestMatchingPlan(cpus, memory, serviceLevel) {
|
||||||
if (!this.pricingData) return null;
|
if (!this.pricingData) return null;
|
||||||
|
@ -488,6 +654,9 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
const storage = parseInt(this.storageRange?.value || 20);
|
const storage = parseInt(this.storageRange?.value || 20);
|
||||||
const instances = parseInt(this.instancesRange?.value || 1);
|
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.showPlanDetails(selectedPlan, storage, instances);
|
||||||
this.updateStatusMessage('Plan selected directly!', 'success');
|
this.updateStatusMessage('Plan selected directly!', 'success');
|
||||||
}
|
}
|
||||||
|
@ -496,6 +665,9 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
updatePricing() {
|
updatePricing() {
|
||||||
if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
|
if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
|
||||||
|
|
||||||
|
// Update addon prices first
|
||||||
|
this.updateAddonPrices();
|
||||||
|
|
||||||
// Reset plan selection if in auto-select mode
|
// Reset plan selection if in auto-select mode
|
||||||
if (!this.planSelect?.value) {
|
if (!this.planSelect?.value) {
|
||||||
const cpus = parseInt(this.cpuRange.value);
|
const cpus = parseInt(this.cpuRange.value);
|
||||||
|
@ -543,16 +715,57 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
if (this.planInstances) this.planInstances.textContent = instances;
|
if (this.planInstances) this.planInstances.textContent = instances;
|
||||||
if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel;
|
if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel;
|
||||||
|
|
||||||
// Calculate pricing using storage price from the plan data
|
// Calculate pricing using final price from plan data (which already includes mandatory addons)
|
||||||
const computePriceValue = parseFloat(plan.compute_plan_price);
|
// plan.final_price = compute_plan_price + sla_price (where sla_price includes mandatory addons)
|
||||||
const servicePriceValue = parseFloat(plan.sla_price);
|
const managedServicePricePerInstance = parseFloat(plan.final_price);
|
||||||
const managedServicePricePerInstance = computePriceValue + servicePriceValue;
|
|
||||||
|
// Collect mandatory addons for display (but don't add to price since they're already included)
|
||||||
|
let mandatoryAddonTotal = 0;
|
||||||
|
const mandatoryAddons = [];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Don't add to mandatoryAddonTotal since it's already in plan.final_price
|
||||||
|
mandatoryAddons.push({
|
||||||
|
name: addon.name,
|
||||||
|
price: calculatedPrice.toFixed(2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const managedServicePrice = managedServicePricePerInstance * instances;
|
const managedServicePrice = managedServicePricePerInstance * instances;
|
||||||
|
|
||||||
// Use storage price from plan data or fallback to instance variable
|
// Use storage price from plan data or fallback to instance variable
|
||||||
const storageUnitPrice = plan.storage_price !== undefined ? parseFloat(plan.storage_price) : this.storagePrice;
|
const storageUnitPrice = plan.storage_price !== undefined ? parseFloat(plan.storage_price) : this.storagePrice;
|
||||||
const storagePriceValue = storage * storageUnitPrice * instances;
|
const storagePriceValue = storage * storageUnitPrice * instances;
|
||||||
const totalPriceValue = managedServicePrice + storagePriceValue;
|
|
||||||
|
// Calculate optional addon total
|
||||||
|
let optionalAddonTotal = 0;
|
||||||
|
const selectedOptionalAddons = [];
|
||||||
|
|
||||||
|
if (this.addonsContainer) {
|
||||||
|
const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox:checked');
|
||||||
|
addonCheckboxes.forEach(checkbox => {
|
||||||
|
const addon = JSON.parse(checkbox.dataset.addon);
|
||||||
|
const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0);
|
||||||
|
|
||||||
|
if (!addon.is_mandatory) {
|
||||||
|
optionalAddonTotal += calculatedPrice;
|
||||||
|
selectedOptionalAddons.push({
|
||||||
|
name: addon.name,
|
||||||
|
price: calculatedPrice.toFixed(2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal;
|
||||||
|
|
||||||
// Update pricing display
|
// Update pricing display
|
||||||
if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2);
|
if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2);
|
||||||
|
@ -560,6 +773,9 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
if (this.storageAmount) this.storageAmount.textContent = storage;
|
if (this.storageAmount) this.storageAmount.textContent = storage;
|
||||||
if (this.totalPrice) this.totalPrice.textContent = totalPriceValue.toFixed(2);
|
if (this.totalPrice) this.totalPrice.textContent = totalPriceValue.toFixed(2);
|
||||||
|
|
||||||
|
// Update addon pricing display
|
||||||
|
this.updateAddonPricingDisplay(mandatoryAddons, selectedOptionalAddons);
|
||||||
|
|
||||||
// Store current configuration for order button
|
// Store current configuration for order button
|
||||||
this.selectedConfiguration = {
|
this.selectedConfiguration = {
|
||||||
planName: plan.compute_plan,
|
planName: plan.compute_plan,
|
||||||
|
@ -569,10 +785,45 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
storage: storage,
|
storage: storage,
|
||||||
instances: instances,
|
instances: instances,
|
||||||
serviceLevel: serviceLevel,
|
serviceLevel: serviceLevel,
|
||||||
totalPrice: totalPriceValue.toFixed(2)
|
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
|
||||||
|
if (mandatoryAddons && mandatoryAddons.length > 0) {
|
||||||
|
mandatoryAddons.forEach(addon => {
|
||||||
|
const addonRow = document.createElement('div');
|
||||||
|
addonRow.className = 'd-flex justify-content-between mb-2';
|
||||||
|
addonRow.innerHTML = `
|
||||||
|
<span>Add-on: ${addon.name} <small class="text-muted">(Required)</small></span>
|
||||||
|
<span class="fw-bold text-muted">CHF ${addon.price}</span>
|
||||||
|
`;
|
||||||
|
this.addonPricingContainer.appendChild(addonRow);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add optional addons to pricing breakdown
|
||||||
|
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
|
// Show no matching plan found
|
||||||
showNoMatch() {
|
showNoMatch() {
|
||||||
if (this.planMatchStatus) this.planMatchStatus.style.display = 'none';
|
if (this.planMatchStatus) this.planMatchStatus.style.display = 'none';
|
||||||
|
|
|
@ -271,7 +271,7 @@
|
||||||
<input type="radio" class="btn-check" name="serviceLevel" id="serviceLevelBestEffort" value="Best Effort" checked>
|
<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>
|
<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>
|
<label class="btn btn-outline-primary" for="serviceLevelGuaranteed">Guaranteed Availability</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -460,342 +460,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 %}
|
{% endblock %}
|
|
@ -377,6 +377,7 @@ def generate_pricing_data(offering):
|
||||||
|
|
||||||
for addon in addons:
|
for addon in addons:
|
||||||
addon_price = None
|
addon_price = None
|
||||||
|
addon_price_per_unit = None
|
||||||
|
|
||||||
if addon.addon_type == "BF": # Base Fee
|
if addon.addon_type == "BF": # Base Fee
|
||||||
addon_price = addon.get_price(currency)
|
addon_price = addon.get_price(currency)
|
||||||
|
@ -392,6 +393,7 @@ def generate_pricing_data(offering):
|
||||||
"commercial_description": addon.commercial_description,
|
"commercial_description": addon.commercial_description,
|
||||||
"addon_type": addon.get_addon_type_display(),
|
"addon_type": addon.get_addon_type_display(),
|
||||||
"price": addon_price,
|
"price": addon_price,
|
||||||
|
"price_per_unit": addon_price_per_unit, # Add per-unit price for frontend calculations
|
||||||
}
|
}
|
||||||
|
|
||||||
if addon.mandatory:
|
if addon.mandatory:
|
||||||
|
@ -428,6 +430,9 @@ def generate_pricing_data(offering):
|
||||||
"storage_price": storage_price_data.get(currency, 0),
|
"storage_price": storage_price_data.get(currency, 0),
|
||||||
"ha_replica_min": appcat_price.ha_replica_min,
|
"ha_replica_min": appcat_price.ha_replica_min,
|
||||||
"ha_replica_max": appcat_price.ha_replica_max,
|
"ha_replica_max": appcat_price.ha_replica_max,
|
||||||
|
"variable_unit": appcat_price.variable_unit,
|
||||||
|
"units": units,
|
||||||
|
"total_units": total_units,
|
||||||
"mandatory_addons": mandatory_addons,
|
"mandatory_addons": mandatory_addons,
|
||||||
"optional_addons": optional_addons,
|
"optional_addons": optional_addons,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue