robustness review of price calc js

This commit is contained in:
Tobias Brunner 2025-07-16 11:23:53 +02:00
parent e7c6a53a17
commit 27c41a6187
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
7 changed files with 143 additions and 24 deletions

View file

@ -53,12 +53,16 @@ class DOMManager {
this.elements.serviceLevelGroup = document.getElementById('serviceLevelGroup');
}
// Get element by key
// Get element by key with error handling
get(key) {
return this.elements[key];
const element = this.elements[key];
if (!element && key !== 'addonsContainer') {
console.warn(`DOM element '${key}' not found`);
}
return element;
}
// Check if element exists
// Check if element exists and is valid
has(key) {
return this.elements[key] && this.elements[key] !== null;
}

View file

@ -46,7 +46,15 @@ class OrderManager {
messageField.value = configMessage;
}
// Store configuration details in hidden field
// Find and fill alternative message field if the first one doesn't exist
if (!messageField) {
const altMessageField = document.querySelector('textarea[name="message"]');
if (altMessageField) {
altMessageField.value = configMessage;
}
}
// Store configuration details in hidden field if it exists
const detailsField = document.querySelector('#order-form input[name="details"]');
if (detailsField) {
detailsField.value = JSON.stringify({

View file

@ -64,7 +64,11 @@ class PlanManager {
if (!planSelect) return;
const serviceLevel = domManager.getSelectedServiceLevel();
if (!serviceLevel) return;
if (!serviceLevel) {
// Clear dropdown if no service level is selected
planSelect.innerHTML = '<option value="">Select a service level first</option>';
return;
}
// Clear existing options
planSelect.innerHTML = '<option value="">Auto-select best matching plan</option>';
@ -72,6 +76,11 @@ class PlanManager {
// Get plans for the selected service level
const availablePlans = this.pricingDataManager.getPlansForServiceLevel(serviceLevel);
if (!availablePlans || availablePlans.length === 0) {
planSelect.innerHTML = '<option value="">No plans available for this service level</option>';
return;
}
// Add plans to dropdown
availablePlans.forEach(plan => {
const option = document.createElement('option');

View file

@ -4,17 +4,27 @@
*/
class PriceCalculator {
constructor() {
// Initialize managers
this.domManager = new DOMManager();
this.currentOffering = this.extractOfferingFromURL();
this.pricingDataManager = new PricingDataManager(this.currentOffering);
this.planManager = new PlanManager(this.pricingDataManager);
this.addonManager = new AddonManager(this.pricingDataManager);
this.uiManager = new UIManager();
this.orderManager = new OrderManager();
try {
// Initialize managers
this.domManager = new DOMManager();
this.currentOffering = this.extractOfferingFromURL();
// Initialize the calculator
this.init();
if (!this.currentOffering) {
throw new Error('Unable to extract offering information from URL');
}
this.pricingDataManager = new PricingDataManager(this.currentOffering);
this.planManager = new PlanManager(this.pricingDataManager);
this.addonManager = new AddonManager(this.pricingDataManager);
this.uiManager = new UIManager();
this.orderManager = new OrderManager();
// Initialize the calculator
this.init();
} catch (error) {
console.error('Error initializing PriceCalculator:', error);
this.showInitializationError(error.message);
}
}
// Extract offering info from URL
@ -41,11 +51,24 @@ class PriceCalculator {
this.orderManager.setupOrderButton(this.domManager);
this.updateCalculator();
} else {
console.warn('No current offering found, calculator not initialized');
throw new Error('No current offering found');
}
} catch (error) {
console.error('Error initializing price calculator:', error);
this.uiManager.showError(this.domManager, 'Failed to load pricing information');
this.showInitializationError(error.message);
}
}
// Show initialization error to user
showInitializationError(message) {
const planMatchStatus = this.domManager?.get('planMatchStatus');
if (planMatchStatus) {
planMatchStatus.innerHTML = `
<i class="bi bi-exclamation-triangle me-2 text-danger"></i>
<span class="text-danger">Failed to load pricing calculator: ${message}</span>
`;
planMatchStatus.className = 'alert alert-danger mb-3';
planMatchStatus.style.display = 'block';
}
}
@ -130,6 +153,23 @@ class PriceCalculator {
window.addEventListener('addon-changed', () => {
this.updatePricing();
});
// Service level change listener
const serviceLevelInputs = this.domManager.get('serviceLevelInputs');
if (serviceLevelInputs) {
serviceLevelInputs.forEach(input => {
input.addEventListener('change', () => {
// Update plan dropdown for new service level
this.planManager.populatePlanDropdown(this.domManager);
// Update addons for new service level
this.addonManager.updateAddons(this.domManager);
// Update pricing
this.updatePricing();
});
});
}
}
// Update calculator (initial setup)

View file

@ -13,7 +13,8 @@ class PricingDataManager {
// Load pricing data from API endpoint
async loadPricingData() {
try {
const response = await fetch(`/offering/${this.currentOffering.provider_slug}/${this.currentOffering.service_slug}/?pricing=json`);
const url = `/offering/${this.currentOffering.provider_slug}/${this.currentOffering.service_slug}/?pricing=json`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load pricing data: ${response.status} ${response.statusText}`);
@ -21,8 +22,17 @@ class PricingDataManager {
const data = await response.json();
if (!data || typeof data !== 'object') {
throw new Error('Invalid pricing data received from server');
}
this.pricingData = data.pricing || data;
// Validate that we have usable pricing data
if (!this.pricingData || Object.keys(this.pricingData).length === 0) {
throw new Error('No pricing data available for this offering');
}
// Extract addons data from the plans - addons are embedded in each plan
this.extractAddonsData();

View file

@ -89,18 +89,18 @@ class UIManager {
const managedServiceIncludesContainer = domManager.get('managedServiceIncludesContainer');
const managedServiceIncludes = domManager.get('managedServiceIncludes');
const managedServiceToggleButton = domManager.get('managedServiceToggleButton');
if (managedServiceIncludesContainer) {
// Clear existing content
managedServiceIncludesContainer.innerHTML = '';
// Show/hide the entire managed service includes section based on mandatory addons
const hasMandatoryAddons = mandatoryAddons && mandatoryAddons.length > 0;
if (managedServiceIncludes) {
managedServiceIncludes.style.display = hasMandatoryAddons ? 'block' : 'none';
}
if (managedServiceToggleButton) {
managedServiceToggleButton.style.display = hasMandatoryAddons ? 'inline-block' : 'none';
}
@ -218,7 +218,7 @@ class UIManager {
// Update the serviceLevelInputs reference
domManager.elements.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]');
// Set up event listeners for the dynamically created service level inputs
this.setupServiceLevelEventListeners(domManager, pricingDataManager);
}

View file

@ -9,7 +9,49 @@
{% block extra_js %}
{% if debug %}
<!-- Development: Load individual modules for easier debugging -->
<script defer src="{% static 'js/price-calculator.js' %}"></script>
{% compress js inline %}
<script src="{% static 'js/price-calculator/dom-manager.js' %}"></script>
<script src="{% static 'js/price-calculator/pricing-data-manager.js' %}"></script>
<script src="{% static 'js/price-calculator/plan-manager.js' %}"></script>
<script src="{% static 'js/price-calculator/addon-manager.js' %}"></script>
<script src="{% static 'js/price-calculator/ui-manager.js' %}"></script>
<script src="{% static 'js/price-calculator/order-manager.js' %}"></script>
<script src="{% static 'js/price-calculator/price-calculator.js' %}"></script>
<script>
// Initialize calculator when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Check if we're on a page that needs the price calculator
if (document.getElementById('cpuRange')) {
try {
window.priceCalculator = new PriceCalculator();
} catch (error) {
console.error('Failed to initialize price calculator:', error);
}
}
});
// Global function for traditional plan selection (used by template buttons)
function selectPlan(element) {
if (!element) return;
const planId = element.getAttribute('data-plan-id');
const planName = element.getAttribute('data-plan-name');
// Find the plan dropdown in the contact form
const planDropdown = document.getElementById('id_choice');
if (planDropdown) {
// Find the option with matching plan id and select it
for (let i = 0; i < planDropdown.options.length; i++) {
const optionValue = planDropdown.options[i].value;
if (optionValue.startsWith(planId + '|')) {
planDropdown.selectedIndex = i;
break;
}
}
}
}
</script>
{% endcompress %}
{% else %}
<!-- Production: Load compressed bundle -->
{% compress js %}
@ -25,12 +67,18 @@
document.addEventListener('DOMContentLoaded', () => {
// Check if we're on a page that needs the price calculator
if (document.getElementById('cpuRange')) {
window.priceCalculator = new PriceCalculator();
try {
window.priceCalculator = new PriceCalculator();
} catch (error) {
console.error('Failed to initialize price calculator:', error);
}
}
});
// Global function for traditional plan selection (used by template buttons)
function selectPlan(element) {
if (!element) return;
const planId = element.getAttribute('data-plan-id');
const planName = element.getAttribute('data-plan-name');