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'); this.elements.serviceLevelGroup = document.getElementById('serviceLevelGroup');
} }
// Get element by key // Get element by key with error handling
get(key) { 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) { has(key) {
return this.elements[key] && this.elements[key] !== null; return this.elements[key] && this.elements[key] !== null;
} }

View file

@ -46,7 +46,15 @@ class OrderManager {
messageField.value = configMessage; 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"]'); const detailsField = document.querySelector('#order-form input[name="details"]');
if (detailsField) { if (detailsField) {
detailsField.value = JSON.stringify({ detailsField.value = JSON.stringify({

View file

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

View file

@ -4,17 +4,27 @@
*/ */
class PriceCalculator { class PriceCalculator {
constructor() { constructor() {
// Initialize managers try {
this.domManager = new DOMManager(); // Initialize managers
this.currentOffering = this.extractOfferingFromURL(); this.domManager = new DOMManager();
this.pricingDataManager = new PricingDataManager(this.currentOffering); this.currentOffering = this.extractOfferingFromURL();
this.planManager = new PlanManager(this.pricingDataManager);
this.addonManager = new AddonManager(this.pricingDataManager);
this.uiManager = new UIManager();
this.orderManager = new OrderManager();
// Initialize the calculator if (!this.currentOffering) {
this.init(); 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 // Extract offering info from URL
@ -41,11 +51,24 @@ class PriceCalculator {
this.orderManager.setupOrderButton(this.domManager); this.orderManager.setupOrderButton(this.domManager);
this.updateCalculator(); this.updateCalculator();
} else { } else {
console.warn('No current offering found, calculator not initialized'); throw new Error('No current offering found');
} }
} catch (error) { } catch (error) {
console.error('Error initializing price calculator:', 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', () => { window.addEventListener('addon-changed', () => {
this.updatePricing(); 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) // Update calculator (initial setup)

View file

@ -13,7 +13,8 @@ class PricingDataManager {
// Load pricing data from API endpoint // Load pricing data from API endpoint
async loadPricingData() { async loadPricingData() {
try { 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) { if (!response.ok) {
throw new Error(`Failed to load pricing data: ${response.status} ${response.statusText}`); throw new Error(`Failed to load pricing data: ${response.status} ${response.statusText}`);
@ -21,8 +22,17 @@ class PricingDataManager {
const data = await response.json(); const data = await response.json();
if (!data || typeof data !== 'object') {
throw new Error('Invalid pricing data received from server');
}
this.pricingData = data.pricing || data; 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 // Extract addons data from the plans - addons are embedded in each plan
this.extractAddonsData(); this.extractAddonsData();

View file

@ -9,7 +9,49 @@
{% block extra_js %} {% block extra_js %}
{% if debug %} {% if debug %}
<!-- Development: Load individual modules for easier debugging --> <!-- 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 %} {% else %}
<!-- Production: Load compressed bundle --> <!-- Production: Load compressed bundle -->
{% compress js %} {% compress js %}
@ -25,12 +67,18 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Check if we're on a page that needs the price calculator // Check if we're on a page that needs the price calculator
if (document.getElementById('cpuRange')) { 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) // Global function for traditional plan selection (used by template buttons)
function selectPlan(element) { function selectPlan(element) {
if (!element) return;
const planId = element.getAttribute('data-plan-id'); const planId = element.getAttribute('data-plan-id');
const planName = element.getAttribute('data-plan-name'); const planName = element.getAttribute('data-plan-name');