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.selectedConfiguration = null;
|
||||
this.replicaInfo = null;
|
||||
this.addonsData = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
|
@ -50,6 +51,10 @@ class PriceCalculator {
|
|||
this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
|
||||
this.planSelect = document.getElementById('planSelect');
|
||||
|
||||
// Addon elements
|
||||
this.addonsContainer = document.getElementById('addonsContainer');
|
||||
this.addonPricingContainer = document.getElementById('addonPricingContainer');
|
||||
|
||||
// Result display elements
|
||||
this.planMatchStatus = document.getElementById('planMatchStatus');
|
||||
this.selectedPlanDetails = document.getElementById('selectedPlanDetails');
|
||||
|
@ -156,25 +161,36 @@ class PriceCalculator {
|
|||
storage: config.storage,
|
||||
instances: config.instances,
|
||||
serviceLevel: config.serviceLevel,
|
||||
totalPrice: config.totalPrice
|
||||
totalPrice: config.totalPrice,
|
||||
addons: config.addons || []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate human-readable configuration message
|
||||
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})
|
||||
vCPUs: ${config.vcpus}
|
||||
Memory: ${config.memory} GB
|
||||
Storage: ${config.storage} GB
|
||||
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.`;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
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
|
||||
this.extractStoragePrice();
|
||||
|
||||
this.setupEventListeners();
|
||||
this.populatePlanDropdown();
|
||||
this.updateAddons();
|
||||
this.updatePricing();
|
||||
} catch (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
|
||||
setupEventListeners() {
|
||||
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', () => {
|
||||
this.updateInstancesSlider();
|
||||
this.populatePlanDropdown();
|
||||
this.updateAddons();
|
||||
this.updatePricing();
|
||||
});
|
||||
});
|
||||
|
@ -269,8 +335,10 @@ Please contact me with next steps for ordering this configuration.`;
|
|||
this.cpuValue.textContent = selectedPlan.vcpus;
|
||||
this.memoryValue.textContent = selectedPlan.ram;
|
||||
|
||||
this.updateAddons();
|
||||
this.updatePricingWithPlan(selectedPlan);
|
||||
} else {
|
||||
this.updateAddons();
|
||||
this.updatePricing();
|
||||
}
|
||||
});
|
||||
|
@ -356,6 +424,7 @@ Please contact me with next steps for ordering this configuration.`;
|
|||
input.addEventListener('change', () => {
|
||||
this.updateInstancesSlider();
|
||||
this.populatePlanDropdown();
|
||||
this.updateAddons();
|
||||
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
|
||||
findBestMatchingPlan(cpus, memory, serviceLevel) {
|
||||
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 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');
|
||||
}
|
||||
|
@ -496,6 +665,9 @@ Please contact me with next steps for ordering this configuration.`;
|
|||
updatePricing() {
|
||||
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
|
||||
if (!this.planSelect?.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.planServiceLevel) this.planServiceLevel.textContent = serviceLevel;
|
||||
|
||||
// Calculate pricing using storage price from the plan data
|
||||
const computePriceValue = parseFloat(plan.compute_plan_price);
|
||||
const servicePriceValue = parseFloat(plan.sla_price);
|
||||
const managedServicePricePerInstance = computePriceValue + servicePriceValue;
|
||||
// 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 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;
|
||||
|
||||
// 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;
|
||||
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
|
||||
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.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,
|
||||
|
@ -569,10 +785,45 @@ Please contact me with next steps for ordering this configuration.`;
|
|||
storage: storage,
|
||||
instances: instances,
|
||||
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
|
||||
showNoMatch() {
|
||||
if (this.planMatchStatus) this.planMatchStatus.style.display = 'none';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue