website/hub/services/static/js/price-calculator.js

501 lines
19 KiB
JavaScript
Raw Normal View History

2025-06-02 16:22:54 +02:00
/**
* Price Calculator for Service Offerings
* Handles interactive pricing calculation with sliders and plan selection
*/
class PriceCalculator {
constructor() {
this.pricingData = null;
this.storagePrice = 0.15; // CHF per GB per month
this.currentOffering = null;
this.selectedConfiguration = null;
2025-06-02 16:22:54 +02:00
this.init();
}
// Initialize calculator elements and event listeners
init() {
// Get offering info from URL
const pathParts = window.location.pathname.split('/');
if (pathParts.length >= 4 && pathParts[1] === 'offering') {
this.currentOffering = {
provider_slug: pathParts[2],
service_slug: pathParts[3]
};
}
// Initialize DOM elements
this.initElements();
// Load pricing data and setup calculator
if (this.currentOffering) {
this.loadPricingData();
}
// Setup order button click handler
this.setupOrderButton();
2025-06-02 16:22:54 +02:00
}
// Initialize DOM element references
initElements() {
// Calculator controls
this.cpuRange = document.getElementById('cpuRange');
this.memoryRange = document.getElementById('memoryRange');
this.storageRange = document.getElementById('storageRange');
this.cpuValue = document.getElementById('cpuValue');
this.memoryValue = document.getElementById('memoryValue');
this.storageValue = document.getElementById('storageValue');
this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
this.planSelect = document.getElementById('planSelect');
// Result display elements
this.planMatchStatus = document.getElementById('planMatchStatus');
this.selectedPlanDetails = document.getElementById('selectedPlanDetails');
this.noMatchFound = document.getElementById('noMatchFound');
// Plan detail elements
this.planGroup = document.getElementById('planGroup');
this.planName = document.getElementById('planName');
this.planDescription = document.getElementById('planDescription');
this.planCpus = document.getElementById('planCpus');
this.planMemory = document.getElementById('planMemory');
2025-06-04 16:30:28 +02:00
this.planServiceLevel = document.getElementById('planServiceLevel');
this.managedServicePrice = document.getElementById('managedServicePrice');
2025-06-02 16:22:54 +02:00
this.storagePriceEl = document.getElementById('storagePrice');
this.storageAmount = document.getElementById('storageAmount');
this.totalPrice = document.getElementById('totalPrice');
// Order button
this.orderButton = document.querySelector('a[href="#order-form"]');
}
// 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,
serviceLevel: config.serviceLevel,
totalPrice: config.totalPrice
});
}
}
// Generate human-readable configuration message
generateConfigurationMessage(config) {
return `I would like to order the following configuration:
Plan: ${config.planName} (${config.planGroup})
vCPUs: ${config.vcpus}
Memory: ${config.memory} GB
Storage: ${config.storage} GB
Service Level: ${config.serviceLevel}
Total Monthly Price: CHF ${config.totalPrice}
Please contact me with next steps for ordering this configuration.`;
2025-06-02 16:22:54 +02:00
}
// 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');
}
this.pricingData = await response.json();
this.setupEventListeners();
this.populatePlanDropdown();
this.updatePricing();
} catch (error) {
console.error('Error loading pricing data:', error);
this.showError('Failed to load pricing information');
}
}
// Setup event listeners for calculator controls
setupEventListeners() {
if (!this.cpuRange || !this.memoryRange || !this.storageRange) 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();
});
// Service level change listeners
this.serviceLevelInputs.forEach(input => {
input.addEventListener('change', () => {
this.populatePlanDropdown();
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;
this.updatePricingWithPlan(selectedPlan);
} else {
this.updatePricing();
}
});
}
}
// 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.populatePlanDropdown();
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.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);
// Update the max display under the slider
const cpuMaxDisplay = this.cpuRange.parentElement.querySelector('.d-flex.justify-content-between .text-muted span:last-child');
if (cpuMaxDisplay) cpuMaxDisplay.textContent = Math.ceil(maxCpus);
}
if (maxMemory > 0) {
this.memoryRange.max = Math.ceil(maxMemory);
// Update the max display under the slider
const memoryMaxDisplay = this.memoryRange.parentElement.querySelector('.d-flex.justify-content-between .text-muted span:last-child');
if (memoryMaxDisplay) memoryMaxDisplay.textContent = Math.ceil(maxMemory) + ' GB';
}
}
// 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`;
2025-06-02 16:22:54 +02:00
this.planSelect.appendChild(option);
});
}
// 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);
this.showPlanDetails(selectedPlan, storage);
this.updateStatusMessage('Plan selected directly!', 'success');
}
// Main pricing update function
updatePricing() {
if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange) return;
// 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 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);
this.updateStatusMessage('Perfect match found!', 'success');
} else {
this.showNoMatch();
}
} else {
// Plan is directly selected, update storage pricing
const selectedPlan = JSON.parse(this.planSelect.value);
this.updatePricingWithPlan(selectedPlan);
}
}
// Show plan details in the UI
showPlanDetails(plan, storage) {
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';
2025-06-04 16:30:28 +02:00
// Get current service level
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value || 'Best Effort';
2025-06-02 16:22:54 +02:00
// 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';
2025-06-04 16:30:28 +02:00
if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel;
2025-06-02 16:22:54 +02:00
// Calculate pricing
const computePriceValue = parseFloat(plan.compute_plan_price);
const servicePriceValue = parseFloat(plan.sla_price);
const managedServicePrice = computePriceValue + servicePriceValue;
2025-06-02 16:22:54 +02:00
const storagePriceValue = storage * this.storagePrice;
const totalPriceValue = managedServicePrice + storagePriceValue;
2025-06-02 16:22:54 +02:00
// Update pricing display
if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2);
2025-06-02 16:22:54 +02:00
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);
// Store current configuration for order button
this.selectedConfiguration = {
planName: plan.compute_plan,
planGroup: plan.groupName,
vcpus: plan.vcpus,
memory: plan.ram,
storage: storage,
serviceLevel: serviceLevel,
totalPrice: totalPriceValue.toFixed(2)
};
2025-06-02 16:22:54 +02:00
}
// 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';
}
}
}
// Initialize calculator when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Only initialize if we're on an offering detail page with pricing calculator
if (document.getElementById('cpuRange')) {
new PriceCalculator();
}
});