improved behaviour
This commit is contained in:
parent
9d423ce61e
commit
3f3b9da992
2 changed files with 164 additions and 40 deletions
|
@ -335,9 +335,18 @@ 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;
|
||||||
|
|
||||||
|
// Fade out CPU and Memory sliders since plan is manually selected
|
||||||
|
this.fadeOutSliders(['cpu', 'memory']);
|
||||||
|
|
||||||
|
// Update addons for the new configuration
|
||||||
this.updateAddons();
|
this.updateAddons();
|
||||||
|
// Update pricing with the selected plan
|
||||||
this.updatePricingWithPlan(selectedPlan);
|
this.updatePricingWithPlan(selectedPlan);
|
||||||
} else {
|
} else {
|
||||||
|
// Auto-select mode - fade sliders back in
|
||||||
|
this.fadeInSliders(['cpu', 'memory']);
|
||||||
|
|
||||||
|
// Auto-select mode - update addons and recalculate
|
||||||
this.updateAddons();
|
this.updateAddons();
|
||||||
this.updatePricing();
|
this.updatePricing();
|
||||||
}
|
}
|
||||||
|
@ -555,6 +564,8 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
if (!addon.is_mandatory) {
|
if (!addon.is_mandatory) {
|
||||||
const checkbox = addonElement.querySelector('.addon-checkbox');
|
const checkbox = addonElement.querySelector('.addon-checkbox');
|
||||||
checkbox.addEventListener('change', () => {
|
checkbox.addEventListener('change', () => {
|
||||||
|
// Update addon prices and recalculate total
|
||||||
|
this.updateAddonPrices();
|
||||||
this.updatePricing();
|
this.updatePricing();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -571,7 +582,7 @@ 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);
|
||||||
|
|
||||||
// Find the current plan data to get variable_unit
|
// Find the current plan data to get variable_unit for addon calculations
|
||||||
const matchedPlan = this.getCurrentPlan();
|
const matchedPlan = this.getCurrentPlan();
|
||||||
const variableUnit = matchedPlan?.variable_unit || 'CPU';
|
const variableUnit = matchedPlan?.variable_unit || 'CPU';
|
||||||
const units = variableUnit === 'CPU' ? cpus : memory;
|
const units = variableUnit === 'CPU' ? cpus : memory;
|
||||||
|
@ -583,17 +594,22 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
const priceElement = checkbox.parentElement.querySelector('.addon-price-value');
|
const priceElement = checkbox.parentElement.querySelector('.addon-price-value');
|
||||||
|
|
||||||
let calculatedPrice = 0;
|
let calculatedPrice = 0;
|
||||||
|
|
||||||
|
// Calculate addon price based on type
|
||||||
if (addon.addon_type === 'BASE_FEE') {
|
if (addon.addon_type === 'BASE_FEE') {
|
||||||
|
// Base fee: price per instance
|
||||||
calculatedPrice = parseFloat(addon.price || 0) * instances;
|
calculatedPrice = parseFloat(addon.price || 0) * instances;
|
||||||
} else if (addon.addon_type === 'UNIT_RATE') {
|
} 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;
|
calculatedPrice = parseFloat(addon.price_per_unit || 0) * totalUnits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the display price
|
||||||
if (priceElement) {
|
if (priceElement) {
|
||||||
priceElement.textContent = calculatedPrice.toFixed(2);
|
priceElement.textContent = calculatedPrice.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the checkbox data for later calculation
|
// Store the calculated price for later use in total calculations
|
||||||
checkbox.dataset.calculatedPrice = calculatedPrice.toString();
|
checkbox.dataset.calculatedPrice = calculatedPrice.toString();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -665,7 +681,7 @@ 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
|
// Update addon prices first to ensure they're current
|
||||||
this.updateAddonPrices();
|
this.updateAddonPrices();
|
||||||
|
|
||||||
// Reset plan selection if in auto-select mode
|
// Reset plan selection if in auto-select mode
|
||||||
|
@ -690,7 +706,13 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
} else {
|
} else {
|
||||||
// Plan is directly selected, update storage pricing
|
// Plan is directly selected, update storage pricing
|
||||||
const selectedPlan = JSON.parse(this.planSelect.value);
|
const selectedPlan = JSON.parse(this.planSelect.value);
|
||||||
this.updatePricingWithPlan(selectedPlan);
|
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -715,13 +737,18 @@ 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;
|
||||||
|
|
||||||
|
// Ensure addon prices are calculated with current configuration
|
||||||
|
this.updateAddonPrices();
|
||||||
|
|
||||||
// Calculate pricing using final price from plan data (which already includes mandatory addons)
|
// 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)
|
// plan.final_price = compute_plan_price + sla_price (where sla_price includes mandatory addons)
|
||||||
const managedServicePricePerInstance = parseFloat(plan.final_price);
|
const managedServicePricePerInstance = parseFloat(plan.final_price);
|
||||||
|
|
||||||
// Collect mandatory addons for display (but don't add to price since they're already included)
|
// Collect addon information for display and calculation
|
||||||
let mandatoryAddonTotal = 0;
|
let mandatoryAddonTotal = 0;
|
||||||
|
let optionalAddonTotal = 0;
|
||||||
const mandatoryAddons = [];
|
const mandatoryAddons = [];
|
||||||
|
const selectedOptionalAddons = [];
|
||||||
|
|
||||||
if (this.addonsContainer) {
|
if (this.addonsContainer) {
|
||||||
const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox');
|
const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox');
|
||||||
|
@ -730,11 +757,19 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0);
|
const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0);
|
||||||
|
|
||||||
if (addon.is_mandatory) {
|
if (addon.is_mandatory) {
|
||||||
// Don't add to mandatoryAddonTotal since it's already in plan.final_price
|
// Mandatory addons are already included in plan.final_price
|
||||||
|
// We collect them for display purposes only
|
||||||
mandatoryAddons.push({
|
mandatoryAddons.push({
|
||||||
name: addon.name,
|
name: addon.name,
|
||||||
price: calculatedPrice.toFixed(2)
|
price: calculatedPrice.toFixed(2)
|
||||||
});
|
});
|
||||||
|
} else if (checkbox.checked) {
|
||||||
|
// Only count checked optional addons
|
||||||
|
optionalAddonTotal += calculatedPrice;
|
||||||
|
selectedOptionalAddons.push({
|
||||||
|
name: addon.name,
|
||||||
|
price: calculatedPrice.toFixed(2)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -745,26 +780,7 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
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;
|
||||||
|
|
||||||
// Calculate optional addon total
|
// Total price = managed service price (includes mandatory addons) + storage + optional addons
|
||||||
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;
|
const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal;
|
||||||
|
|
||||||
// Update pricing display
|
// Update pricing display
|
||||||
|
@ -797,20 +813,33 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
// Clear existing addon pricing display
|
// Clear existing addon pricing display
|
||||||
this.addonPricingContainer.innerHTML = '';
|
this.addonPricingContainer.innerHTML = '';
|
||||||
|
|
||||||
// Add mandatory addons to pricing breakdown
|
// Add mandatory addons to pricing breakdown (for informational purposes only)
|
||||||
if (mandatoryAddons && mandatoryAddons.length > 0) {
|
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 => {
|
mandatoryAddons.forEach(addon => {
|
||||||
const addonRow = document.createElement('div');
|
const addonRow = document.createElement('div');
|
||||||
addonRow.className = 'd-flex justify-content-between mb-2';
|
addonRow.className = 'd-flex justify-content-between mb-1 ps-3';
|
||||||
addonRow.innerHTML = `
|
addonRow.innerHTML = `
|
||||||
<span>Add-on: ${addon.name} <small class="text-muted">(Required)</small></span>
|
<span class="text-muted small">${addon.name}</span>
|
||||||
<span class="fw-bold text-muted">CHF ${addon.price}</span>
|
<span class="text-muted small">CHF ${addon.price}</span>
|
||||||
`;
|
`;
|
||||||
this.addonPricingContainer.appendChild(addonRow);
|
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
|
// Add optional addons to pricing breakdown (these are added to total)
|
||||||
if (selectedOptionalAddons && selectedOptionalAddons.length > 0) {
|
if (selectedOptionalAddons && selectedOptionalAddons.length > 0) {
|
||||||
selectedOptionalAddons.forEach(addon => {
|
selectedOptionalAddons.forEach(addon => {
|
||||||
const addonRow = document.createElement('div');
|
const addonRow = document.createElement('div');
|
||||||
|
@ -852,6 +881,58 @@ Please contact me with next steps for ordering this configuration.`;
|
||||||
this.planMatchStatus.style.display = 'block';
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize calculator when DOM is loaded
|
// Initialize calculator when DOM is loaded
|
||||||
|
|
|
@ -203,13 +203,56 @@ def pricelist(request):
|
||||||
discount_savings = 0
|
discount_savings = 0
|
||||||
discount_percentage = 0
|
discount_percentage = 0
|
||||||
|
|
||||||
# Get addon information
|
# Calculate final price using the model method to ensure consistency
|
||||||
addons = appcat_price.addons.filter(active=True)
|
price_calculation = appcat_price.calculate_final_price(
|
||||||
|
currency_code=currency,
|
||||||
|
service_level=service_level,
|
||||||
|
number_of_units=total_units,
|
||||||
|
addon_ids=None, # This will include only mandatory addons
|
||||||
|
)
|
||||||
|
|
||||||
|
if price_calculation is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate base service price (without addons) for display purposes
|
||||||
|
base_sla_price = base_fee + (total_units * unit_rate)
|
||||||
|
|
||||||
|
# Apply discount if available
|
||||||
|
discount_breakdown = None
|
||||||
|
if (
|
||||||
|
appcat_price.discount_model
|
||||||
|
and appcat_price.discount_model.active
|
||||||
|
):
|
||||||
|
discounted_price = (
|
||||||
|
appcat_price.discount_model.calculate_discount(
|
||||||
|
unit_rate, total_units
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sla_price = base_fee + discounted_price
|
||||||
|
discount_savings = base_sla_price - sla_price
|
||||||
|
discount_percentage = (
|
||||||
|
(discount_savings / base_sla_price) * 100
|
||||||
|
if base_sla_price > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
discount_breakdown = (
|
||||||
|
appcat_price.discount_model.get_discount_breakdown(
|
||||||
|
unit_rate, total_units
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sla_price = base_sla_price
|
||||||
|
discounted_price = total_units * unit_rate
|
||||||
|
discount_savings = 0
|
||||||
|
discount_percentage = 0
|
||||||
|
|
||||||
|
# Extract addon information from the calculation
|
||||||
mandatory_addons = []
|
mandatory_addons = []
|
||||||
optional_addons = []
|
optional_addons = []
|
||||||
|
|
||||||
# Group addons by mandatory vs optional
|
# Get all addons to separate mandatory from optional
|
||||||
for addon in addons:
|
all_addons = appcat_price.addons.filter(active=True)
|
||||||
|
for addon in all_addons:
|
||||||
addon_price = None
|
addon_price = None
|
||||||
|
|
||||||
if addon.addon_type == "BF": # Base Fee
|
if addon.addon_type == "BF": # Base Fee
|
||||||
|
@ -232,12 +275,12 @@ def pricelist(request):
|
||||||
|
|
||||||
if addon.mandatory:
|
if addon.mandatory:
|
||||||
mandatory_addons.append(addon_info)
|
mandatory_addons.append(addon_info)
|
||||||
if addon_price:
|
|
||||||
sla_price += addon_price
|
|
||||||
else:
|
else:
|
||||||
optional_addons.append(addon_info)
|
optional_addons.append(addon_info)
|
||||||
|
|
||||||
final_price = compute_plan_price + sla_price
|
# Use the calculated total price which includes mandatory addons
|
||||||
|
service_price_with_addons = price_calculation["total_price"]
|
||||||
|
final_price = compute_plan_price + service_price_with_addons
|
||||||
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
|
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
|
||||||
service_level
|
service_level
|
||||||
]
|
]
|
||||||
|
@ -309,8 +352,8 @@ def pricelist(request):
|
||||||
"service_level": service_level_display,
|
"service_level": service_level_display,
|
||||||
"sla_base": base_fee,
|
"sla_base": base_fee,
|
||||||
"sla_per_unit": unit_rate,
|
"sla_per_unit": unit_rate,
|
||||||
"sla_price": sla_price,
|
"sla_price": service_price_with_addons,
|
||||||
"standard_sla_price": standard_sla_price,
|
"standard_sla_price": base_sla_price,
|
||||||
"discounted_sla_price": (
|
"discounted_sla_price": (
|
||||||
base_fee + discounted_price
|
base_fee + discounted_price
|
||||||
if appcat_price.discount_model
|
if appcat_price.discount_model
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue