diff --git a/hub/services/admin/pricing.py b/hub/services/admin/pricing.py index 6da4852..1077b68 100644 --- a/hub/services/admin/pricing.py +++ b/hub/services/admin/pricing.py @@ -2,13 +2,8 @@ Admin classes for pricing models including compute plans, storage plans, and VSHN AppCat pricing """ -import re -from django.contrib import admin, messages -from django.contrib.admin import helpers +from django.contrib import admin from django.utils.html import format_html -from django import forms -from django.shortcuts import render -from django.http import HttpResponseRedirect from adminsortable2.admin import SortableAdminMixin from import_export.admin import ImportExportModelAdmin from import_export import resources @@ -31,14 +26,6 @@ from ..models import ( Service, ) -from ..models.base import Term - - -def natural_sort_key(obj): - """Extract numeric parts for natural sorting""" - parts = re.split(r"(\d+)", obj.name) - return [int(part) if part.isdigit() else part for part in parts] - class ComputePlanPriceInline(admin.TabularInline): """Inline admin for ComputePlanPrice model""" @@ -129,43 +116,8 @@ class ComputePlanResource(resources.ModelResource): ) -class MassUpdateComputePlanForm(forms.Form): - """Form for mass updating ComputePlan fields""" - - active = forms.ChoiceField( - choices=[("", "-- No change --"), ("True", "Active"), ("False", "Inactive")], - required=False, - help_text="Set active status for selected compute plans", - ) - - term = forms.ChoiceField( - choices=[("", "-- No change --")] + Term.choices, - required=False, - help_text="Set billing term for selected compute plans", - ) - - group = forms.ModelChoiceField( - queryset=ComputePlanGroup.objects.all(), - required=False, - empty_label="-- No change --", - help_text="Set group for selected compute plans", - ) - - valid_from = forms.DateField( - widget=forms.DateInput(attrs={"type": "date"}), - required=False, - help_text="Set valid from date for selected compute plans", - ) - - valid_to = forms.DateField( - widget=forms.DateInput(attrs={"type": "date"}), - required=False, - help_text="Set valid to date for selected compute plans", - ) - - @admin.register(ComputePlan) -class ComputePlanAdmin(ImportExportModelAdmin): +class ComputePlansAdmin(ImportExportModelAdmin): """Admin configuration for ComputePlan model with import/export functionality""" resource_class = ComputePlanResource @@ -181,21 +133,8 @@ class ComputePlanAdmin(ImportExportModelAdmin): ) search_fields = ("name", "cloud_provider__name", "group__name") list_filter = ("active", "cloud_provider", "group") + ordering = ("name",) inlines = [ComputePlanPriceInline] - actions = ["mass_update_compute_plans"] - - def changelist_view(self, request, extra_context=None): - """Override changelist view to apply natural sorting""" - # Get the response from parent - response = super().changelist_view(request, extra_context) - - # If it's a TemplateResponse, we can modify the context - if hasattr(response, "context_data") and "cl" in response.context_data: - cl = response.context_data["cl"] - if hasattr(cl, "result_list"): - cl.result_list = sorted(cl.result_list, key=natural_sort_key) - - return response def display_prices(self, obj): """Display formatted prices for the list view""" @@ -206,80 +145,6 @@ class ComputePlanAdmin(ImportExportModelAdmin): display_prices.short_description = "Prices (Amount Currency)" - def mass_update_compute_plans(self, request, queryset): - """Admin action to mass update compute plan fields""" - if request.POST.get("post"): - # Process the form submission - form = MassUpdateComputePlanForm(request.POST) - if form.is_valid(): - updated_count = 0 - updated_fields = [] - - # Prepare update data - update_data = {} - - # Handle active field - if form.cleaned_data["active"]: - update_data["active"] = form.cleaned_data["active"] == "True" - updated_fields.append("active") - - # Handle term field - if form.cleaned_data["term"]: - update_data["term"] = form.cleaned_data["term"] - updated_fields.append("term") - - # Handle group field - if form.cleaned_data["group"]: - update_data["group"] = form.cleaned_data["group"] - updated_fields.append("group") - - # Handle valid_from field - if form.cleaned_data["valid_from"]: - update_data["valid_from"] = form.cleaned_data["valid_from"] - updated_fields.append("valid_from") - - # Handle valid_to field - if form.cleaned_data["valid_to"]: - update_data["valid_to"] = form.cleaned_data["valid_to"] - updated_fields.append("valid_to") - - # Perform the bulk update - if update_data: - updated_count = queryset.update(**update_data) - - # Create success message - field_list = ", ".join(updated_fields) - self.message_user( - request, - f"Successfully updated {updated_count} compute plan(s). " - f"Updated fields: {field_list}", - messages.SUCCESS, - ) - else: - self.message_user( - request, "No fields were selected for update.", messages.WARNING - ) - - return HttpResponseRedirect(request.get_full_path()) - else: - # Show the form - form = MassUpdateComputePlanForm() - - # Render the mass update template - return render( - request, - "admin/mass_update_compute_plans.html", - { - "form": form, - "queryset": queryset, - "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME, - "opts": self.model._meta, - "title": f"Mass Update {queryset.count()} Compute Plans", - }, - ) - - mass_update_compute_plans.short_description = "Mass update selected compute plans" - class VSHNAppCatBaseFeeInline(admin.TabularInline): """Inline admin for VSHNAppCatBaseFee model""" @@ -326,7 +191,6 @@ class VSHNAppCatPriceAdmin(admin.ModelAdmin): "discount_model", "admin_display_base_fees", "admin_display_unit_rates", - "public_display_enabled", ) list_filter = ("variable_unit", "service", "discount_model") search_fields = ("service__name",) diff --git a/hub/services/migrations/0033_vshnappcatprice_public_display_enabled_and_more.py b/hub/services/migrations/0033_vshnappcatprice_public_display_enabled_and_more.py deleted file mode 100644 index 6089f11..0000000 --- a/hub/services/migrations/0033_vshnappcatprice_public_display_enabled_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2 on 2025-06-04 15:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("services", "0032_externalpriceplans_service_level"), - ] - - operations = [ - migrations.AddField( - model_name="vshnappcatprice", - name="public_display_enabled", - field=models.BooleanField( - default=True, - help_text="Enable public display of price calculator on offering detail page", - ), - ), - migrations.AlterField( - model_name="externalpriceplans", - name="compare_to", - field=models.ManyToManyField( - blank=True, related_name="external_prices", to="services.computeplan" - ), - ), - ] diff --git a/hub/services/models/pricing.py b/hub/services/models/pricing.py index 42b2778..dc557ea 100644 --- a/hub/services/models/pricing.py +++ b/hub/services/models/pricing.py @@ -310,11 +310,6 @@ class VSHNAppCatPrice(models.Model): default=1, help_text="Maximum supported replicas" ) - public_display_enabled = models.BooleanField( - default=True, - help_text="Enable public display of price calculator on offering detail page", - ) - valid_from = models.DateTimeField(blank=True, null=True) valid_to = models.DateTimeField(blank=True, null=True) diff --git a/hub/services/static/css/price-calculator.css b/hub/services/static/css/price-calculator.css deleted file mode 100644 index de8074e..0000000 --- a/hub/services/static/css/price-calculator.css +++ /dev/null @@ -1,36 +0,0 @@ -.form-range::-webkit-slider-thumb { - background: #6f42c1; -} - -.form-range::-moz-range-thumb { - background: #6f42c1; - border: none; -} - -.btn-check:checked+.btn-outline-primary { - background-color: #6f42c1; - border-color: #6f42c1; - color: white; -} - -.card { - transition: box-shadow 0.2s ease-in-out; -} - -.card:hover { - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); -} - -#selectedPlanDetails { - animation: fadeIn 0.3s ease-in; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} \ No newline at end of file diff --git a/hub/services/static/js/price-calculator.js b/hub/services/static/js/price-calculator.js deleted file mode 100644 index a65a9f2..0000000 --- a/hub/services/static/js/price-calculator.js +++ /dev/null @@ -1,612 +0,0 @@ -/** - * Price Calculator for Service Offerings - * Handles interactive pricing calculation with sliders and plan selection - */ - -class PriceCalculator { - constructor() { - this.pricingData = null; - this.storagePrice = null; - this.currentOffering = null; - this.selectedConfiguration = null; - this.replicaInfo = null; - 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(); - } - - // Initialize DOM element references - initElements() { - // Calculator controls - this.cpuRange = document.getElementById('cpuRange'); - this.memoryRange = document.getElementById('memoryRange'); - this.storageRange = document.getElementById('storageRange'); - this.instancesRange = document.getElementById('instancesRange'); - this.cpuValue = document.getElementById('cpuValue'); - this.memoryValue = document.getElementById('memoryValue'); - this.storageValue = document.getElementById('storageValue'); - this.instancesValue = document.getElementById('instancesValue'); - 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'); - this.planInstances = document.getElementById('planInstances'); - this.planServiceLevel = document.getElementById('planServiceLevel'); - this.managedServicePrice = document.getElementById('managedServicePrice'); - 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"]'); - } - - // Update slider display values (min/max text below sliders) - updateSliderDisplayValues() { - // Update CPU slider display - if (this.cpuRange) { - const cpuMinDisplay = document.getElementById('cpuMinDisplay'); - const cpuMaxDisplay = document.getElementById('cpuMaxDisplay'); - if (cpuMinDisplay) cpuMinDisplay.textContent = this.cpuRange.min; - if (cpuMaxDisplay) cpuMaxDisplay.textContent = this.cpuRange.max; - } - - // Update Memory slider display - if (this.memoryRange) { - const memoryMinDisplay = document.getElementById('memoryMinDisplay'); - const memoryMaxDisplay = document.getElementById('memoryMaxDisplay'); - if (memoryMinDisplay) memoryMinDisplay.textContent = this.memoryRange.min + ' GB'; - if (memoryMaxDisplay) memoryMaxDisplay.textContent = this.memoryRange.max + ' GB'; - } - - // Update Storage slider display - if (this.storageRange) { - const storageMinDisplay = document.getElementById('storageMinDisplay'); - const storageMaxDisplay = document.getElementById('storageMaxDisplay'); - if (storageMinDisplay) storageMinDisplay.textContent = this.storageRange.min + ' GB'; - if (storageMaxDisplay) storageMaxDisplay.textContent = this.storageRange.max + ' GB'; - } - - // Update Instances slider display - if (this.instancesRange) { - const instancesMinDisplay = document.getElementById('instancesMinDisplay'); - const instancesMaxDisplay = document.getElementById('instancesMaxDisplay'); - if (instancesMinDisplay) instancesMinDisplay.textContent = this.instancesRange.min; - if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.instancesRange.max; - } - } - - // 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, - instances: config.instances, - 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 -Instances: ${config.instances} -Service Level: ${config.serviceLevel} - -Total Monthly Price: CHF ${config.totalPrice} - -Please contact me with next steps for ordering this configuration.`; - } - - // 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(); - - // Extract storage price from the first available plan - this.extractStoragePrice(); - - this.setupEventListeners(); - this.populatePlanDropdown(); - this.updatePricing(); - } catch (error) { - console.error('Error loading pricing data:', error); - this.showError('Failed to load pricing information'); - } - } - - // Extract replica information and storage price from pricing data - extractStoragePrice() { - if (!this.pricingData) return; - - // Find the first plan with storage pricing data and replica info - for (const groupName of Object.keys(this.pricingData)) { - const group = this.pricingData[groupName]; - for (const serviceLevel of Object.keys(group)) { - const plans = group[serviceLevel]; - if (plans.length > 0 && plans[0].storage_price !== undefined) { - this.storagePrice = parseFloat(plans[0].storage_price); - this.replicaInfo = { - ha_replica_min: plans[0].ha_replica_min || 1, - ha_replica_max: plans[0].ha_replica_max || 1 - }; - return; - } - } - } - } - - // Setup event listeners for calculator controls - setupEventListeners() { - if (!this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) 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(); - }); - - this.instancesRange.addEventListener('input', () => { - this.instancesValue.textContent = this.instancesRange.value; - this.updatePricing(); - }); - - // Service level change listeners - this.serviceLevelInputs.forEach(input => { - input.addEventListener('change', () => { - this.updateInstancesSlider(); - 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(); - } - }); - } - - // Initialize instances slider - this.updateInstancesSlider(); - } - - // Update instances slider based on service level and replica info - updateInstancesSlider() { - if (!this.instancesRange || !this.replicaInfo) return; - - const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; - - if (serviceLevel === 'Guaranteed Availability') { - // For GA, min is ha_replica_min - this.instancesRange.min = this.replicaInfo.ha_replica_min; - this.instancesRange.value = Math.max(this.instancesRange.value, this.replicaInfo.ha_replica_min); - } else { - // For BE, min is 1 - this.instancesRange.min = 1; - this.instancesRange.value = Math.max(this.instancesRange.value, 1); - } - - // Set max to ha_replica_max - this.instancesRange.max = this.replicaInfo.ha_replica_max; - - // Update display value - this.instancesValue.textContent = this.instancesRange.value; - - // Update the min/max display under the slider using direct IDs - const instancesMinDisplay = document.getElementById('instancesMinDisplay'); - const instancesMaxDisplay = document.getElementById('instancesMaxDisplay'); - - if (instancesMinDisplay) instancesMinDisplay.textContent = this.instancesRange.min; - if (instancesMaxDisplay) instancesMaxDisplay.textContent = this.instancesRange.max; - } - - // 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.updateInstancesSlider(); - 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 will call updateSliderDisplayValues() - 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); - } - - if (maxMemory > 0) { - this.memoryRange.max = Math.ceil(maxMemory); - } - - // Update display values after changing min/max - moved to end and call explicitly - this.updateSliderDisplayValues(); - } - - // 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 = ''; - - // 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`; - 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); - const instances = parseInt(this.instancesRange?.value || 1); - - this.showPlanDetails(selectedPlan, storage, instances); - this.updateStatusMessage('Plan selected directly!', 'success'); - } - - // Main pricing update function - updatePricing() { - if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) 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 instances = parseInt(this.instancesRange.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, instances); - 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, instances) { - 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'; - - // Get current service level - const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value || 'Best Effort'; - - // 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'; - 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; - 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; - - // Update pricing display - if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2); - 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, - instances: instances, - serviceLevel: serviceLevel, - totalPrice: totalPriceValue.toFixed(2) - }; - } - - // 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 = `${message}`; - this.planMatchStatus.className = `alert ${alertClass} mb-3`; - this.planMatchStatus.style.display = 'block'; - } - - // Show error message - showError(message) { - if (this.planMatchStatus) { - this.planMatchStatus.innerHTML = `${message}`; - 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(); - } -}); \ No newline at end of file diff --git a/hub/services/templates/admin/mass_update_compute_plans.html b/hub/services/templates/admin/mass_update_compute_plans.html deleted file mode 100644 index df2c558..0000000 --- a/hub/services/templates/admin/mass_update_compute_plans.html +++ /dev/null @@ -1,126 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n admin_urls static admin_modify %} - -{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} - -{% block breadcrumbs %} -
-{% endblock %} - -{% block content %} -You are about to update {{ queryset.count }} compute plan(s). Please select the fields you want to update:
- - -Selecting a plan will override the slider configuration
-Interested in a custom plan? Let us know via the contact form.
-{{ representative_plan.compute_plan_group_description }}
+ {% endif %} + {% endwith %} + {% endif %} + {% if forloop.first %} + {% comment %} Only show description for first service level {% endcomment %} + {% endif %} + {% endfor %} - -