add addons to services
This commit is contained in:
parent
b96b186875
commit
22e527bcd9
8 changed files with 1039 additions and 4 deletions
|
@ -275,6 +275,14 @@
|
|||
<label class="btn btn-outline-primary" for="serviceLevelGuaranteed">Guaranteed Availability</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Addons Section -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Add-ons (Optional)</label>
|
||||
<div id="addonsContainer">
|
||||
<!-- Add-ons will be dynamically populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Direct Plan Selection -->
|
||||
<div class="mb-4">
|
||||
|
@ -345,6 +353,12 @@
|
|||
<span>Storage - <span id="storageAmount">20</span> GB</span>
|
||||
<span class="fw-bold">CHF <span id="storagePrice">0.00</span></span>
|
||||
</div>
|
||||
|
||||
<!-- Addons Pricing -->
|
||||
<div id="addonPricingContainer">
|
||||
<!-- Addon pricing will be dynamically added here -->
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="fs-5 fw-bold">Total Monthly Price</span>
|
||||
|
@ -447,5 +461,341 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- JavaScript for the price calculator with addons support -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Only run this script when the price calculator is present
|
||||
if (!document.getElementById('cpuRange')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch pricing data from the server
|
||||
fetch(window.location.href + '?pricing=json')
|
||||
.then(response => response.json())
|
||||
.then(pricingData => {
|
||||
initializePriceCalculator(pricingData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching pricing data:', error);
|
||||
});
|
||||
|
||||
function initializePriceCalculator(pricingData) {
|
||||
// Store selected addons
|
||||
let selectedAddons = [];
|
||||
|
||||
// UI Controls
|
||||
const cpuRange = document.getElementById('cpuRange');
|
||||
const memoryRange = document.getElementById('memoryRange');
|
||||
const storageRange = document.getElementById('storageRange');
|
||||
const instancesRange = document.getElementById('instancesRange');
|
||||
const serviceLevelGroup = document.getElementById('serviceLevelGroup');
|
||||
const planSelect = document.getElementById('planSelect');
|
||||
const addonsContainer = document.getElementById('addonsContainer');
|
||||
const addonPricingContainer = document.getElementById('addonPricingContainer');
|
||||
|
||||
// Results UI elements
|
||||
const planMatchStatus = document.getElementById('planMatchStatus');
|
||||
const selectedPlanDetails = document.getElementById('selectedPlanDetails');
|
||||
const noMatchFound = document.getElementById('noMatchFound');
|
||||
const planName = document.getElementById('planName');
|
||||
const planGroup = document.getElementById('planGroup');
|
||||
const planDescription = document.getElementById('planDescription');
|
||||
const planCpus = document.getElementById('planCpus');
|
||||
const planMemory = document.getElementById('planMemory');
|
||||
const planInstances = document.getElementById('planInstances');
|
||||
const planServiceLevel = document.getElementById('planServiceLevel');
|
||||
const managedServicePrice = document.getElementById('managedServicePrice');
|
||||
const storagePrice = document.getElementById('storagePrice');
|
||||
const storageAmount = document.getElementById('storageAmount');
|
||||
const totalPrice = document.getElementById('totalPrice');
|
||||
|
||||
// Find all plan options for the select dropdown
|
||||
populatePlanOptions(pricingData);
|
||||
|
||||
// Populate optional addons
|
||||
populateAddons(pricingData);
|
||||
|
||||
// Set up event listeners
|
||||
cpuRange.addEventListener('input', updateCalculator);
|
||||
memoryRange.addEventListener('input', updateCalculator);
|
||||
storageRange.addEventListener('input', updateCalculator);
|
||||
instancesRange.addEventListener('input', updateCalculator);
|
||||
planSelect.addEventListener('change', handlePlanSelection);
|
||||
|
||||
// Add listeners for service level radio buttons
|
||||
const serviceLevelRadios = document.querySelectorAll('input[name="serviceLevel"]');
|
||||
serviceLevelRadios.forEach(radio => {
|
||||
radio.addEventListener('change', updateCalculator);
|
||||
});
|
||||
|
||||
// Initial calculation
|
||||
updateCalculator();
|
||||
|
||||
function populatePlanOptions(pricingData) {
|
||||
const planOption = document.createElement('option');
|
||||
planOption.value = '';
|
||||
planOption.textContent = 'Auto-select best matching plan';
|
||||
planSelect.appendChild(planOption);
|
||||
|
||||
// Add all available plans to the dropdown
|
||||
Object.keys(pricingData).forEach(groupName => {
|
||||
const group = pricingData[groupName];
|
||||
|
||||
// Create optgroup for the plan group
|
||||
const optgroup = document.createElement('optgroup');
|
||||
optgroup.label = groupName;
|
||||
|
||||
// Add plans from each service level
|
||||
Object.keys(group).forEach(serviceLevel => {
|
||||
const plans = group[serviceLevel];
|
||||
|
||||
plans.forEach(plan => {
|
||||
const option = document.createElement('option');
|
||||
option.value = `${plan.compute_plan}|${serviceLevel}`;
|
||||
option.textContent = `${plan.compute_plan} (${serviceLevel}) - ${plan.vcpus} vCPU, ${plan.ram} GB RAM`;
|
||||
option.dataset.planData = JSON.stringify(plan);
|
||||
optgroup.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
planSelect.appendChild(optgroup);
|
||||
});
|
||||
}
|
||||
|
||||
function populateAddons(pricingData) {
|
||||
// Get a sample plan to extract addons (assuming all plans have the same addons)
|
||||
let samplePlan = null;
|
||||
for (const groupName in pricingData) {
|
||||
for (const serviceLevel in pricingData[groupName]) {
|
||||
if (pricingData[groupName][serviceLevel].length > 0) {
|
||||
samplePlan = pricingData[groupName][serviceLevel][0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (samplePlan) break;
|
||||
}
|
||||
|
||||
if (!samplePlan || !samplePlan.optional_addons) {
|
||||
addonsContainer.innerHTML = '<p class="text-muted">No optional add-ons available</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create UI for each optional addon
|
||||
samplePlan.optional_addons.forEach(addon => {
|
||||
const addonDiv = document.createElement('div');
|
||||
addonDiv.className = 'form-check mb-2';
|
||||
|
||||
const addonCheckbox = document.createElement('input');
|
||||
addonCheckbox.type = 'checkbox';
|
||||
addonCheckbox.className = 'form-check-input addon-checkbox';
|
||||
addonCheckbox.id = `addon-${addon.id}`;
|
||||
addonCheckbox.dataset.addonId = addon.id;
|
||||
addonCheckbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
selectedAddons.push(addon.id);
|
||||
} else {
|
||||
selectedAddons = selectedAddons.filter(id => id !== addon.id);
|
||||
}
|
||||
updateCalculator();
|
||||
});
|
||||
|
||||
const addonLabel = document.createElement('label');
|
||||
addonLabel.className = 'form-check-label';
|
||||
addonLabel.htmlFor = `addon-${addon.id}`;
|
||||
addonLabel.innerHTML = `${addon.name} <small class="text-muted">${addon.commercial_description || addon.description || ''}</small>`;
|
||||
|
||||
addonDiv.appendChild(addonCheckbox);
|
||||
addonDiv.appendChild(addonLabel);
|
||||
addonsContainer.appendChild(addonDiv);
|
||||
});
|
||||
|
||||
if (samplePlan.optional_addons.length === 0) {
|
||||
addonsContainer.innerHTML = '<p class="text-muted">No optional add-ons available</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateCalculator() {
|
||||
// If a specific plan is selected, use that
|
||||
if (planSelect.value) {
|
||||
handlePlanSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current values from UI
|
||||
const cpuValue = parseInt(cpuRange.value);
|
||||
const memoryValue = parseInt(memoryRange.value);
|
||||
const storageValue = parseInt(storageRange.value);
|
||||
const instancesValue = parseInt(instancesRange.value);
|
||||
const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked').value;
|
||||
|
||||
// Find the best matching plan
|
||||
const bestPlan = findBestMatchingPlan(cpuValue, memoryValue, serviceLevel);
|
||||
|
||||
if (bestPlan) {
|
||||
displayPlanDetails(bestPlan, storageValue, serviceLevel);
|
||||
} else {
|
||||
// No matching plan found
|
||||
planMatchStatus.style.display = 'none';
|
||||
selectedPlanDetails.style.display = 'none';
|
||||
noMatchFound.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function handlePlanSelection() {
|
||||
if (!planSelect.value) {
|
||||
updateCalculator();
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = planSelect.options[planSelect.selectedIndex];
|
||||
const planData = JSON.parse(selectedOption.dataset.planData);
|
||||
|
||||
// Get current values from UI
|
||||
const storageValue = parseInt(storageRange.value);
|
||||
const serviceLevel = planData.service_level;
|
||||
|
||||
// Update service level radio buttons
|
||||
document.querySelectorAll('input[name="serviceLevel"]').forEach(radio => {
|
||||
if (radio.value === serviceLevel) {
|
||||
radio.checked = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update sliders to match selected plan
|
||||
cpuRange.value = planData.vcpus;
|
||||
document.getElementById('cpuValue').textContent = planData.vcpus;
|
||||
|
||||
memoryRange.value = planData.ram;
|
||||
document.getElementById('memoryValue').textContent = planData.ram;
|
||||
|
||||
displayPlanDetails(planData, storageValue, serviceLevel);
|
||||
}
|
||||
|
||||
function findBestMatchingPlan(cpuValue, memoryValue, serviceLevel) {
|
||||
let bestMatch = null;
|
||||
let bestMatchScore = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// Search through all groups and plans
|
||||
Object.keys(pricingData).forEach(groupName => {
|
||||
const group = pricingData[groupName];
|
||||
|
||||
if (group[serviceLevel]) {
|
||||
group[serviceLevel].forEach(plan => {
|
||||
// Calculate how well this plan matches the requirements
|
||||
const cpuDiff = Math.abs(plan.vcpus - cpuValue);
|
||||
const memoryDiff = Math.abs(plan.ram - memoryValue);
|
||||
|
||||
// Simple scoring: sum of differences, lower is better
|
||||
const score = cpuDiff + memoryDiff;
|
||||
|
||||
// Check if this plan meets minimum requirements
|
||||
if (plan.vcpus >= cpuValue && plan.ram >= memoryValue) {
|
||||
// If this is a better match than the current best
|
||||
if (score < bestMatchScore) {
|
||||
bestMatch = plan;
|
||||
bestMatchScore = score;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
function displayPlanDetails(plan, storageValue, serviceLevel) {
|
||||
// Update UI to show selected plan
|
||||
planMatchStatus.style.display = 'none';
|
||||
selectedPlanDetails.style.display = 'block';
|
||||
noMatchFound.style.display = 'none';
|
||||
|
||||
// Set plan details
|
||||
planName.textContent = plan.compute_plan;
|
||||
planGroup.textContent = plan.compute_plan_group;
|
||||
planDescription.textContent = plan.compute_plan_group_description || '';
|
||||
planCpus.textContent = plan.vcpus;
|
||||
planMemory.textContent = plan.ram + ' GB';
|
||||
planInstances.textContent = '1'; // Default to 1 instance
|
||||
planServiceLevel.textContent = serviceLevel;
|
||||
|
||||
// Calculate prices
|
||||
const storageCost = (storageValue * plan.storage_price).toFixed(2);
|
||||
const totalMonthlyCost = (plan.final_price + parseFloat(storageCost)).toFixed(2);
|
||||
|
||||
// Update price displays
|
||||
managedServicePrice.textContent = plan.final_price.toFixed(2);
|
||||
storagePrice.textContent = storageCost;
|
||||
storageAmount.textContent = storageValue;
|
||||
|
||||
// Process addons
|
||||
updateAddonPricing(plan, serviceLevel);
|
||||
|
||||
// Update total price after addons are processed
|
||||
calculateTotalPrice(plan, storageCost);
|
||||
}
|
||||
|
||||
function updateAddonPricing(plan, serviceLevel) {
|
||||
// Clear previous addon pricing
|
||||
addonPricingContainer.innerHTML = '';
|
||||
|
||||
// Display mandatory addons first
|
||||
if (plan.mandatory_addons && plan.mandatory_addons.length > 0) {
|
||||
plan.mandatory_addons.forEach(addon => {
|
||||
if (addon.price) {
|
||||
const addonDiv = document.createElement('div');
|
||||
addonDiv.className = 'd-flex justify-content-between mb-2';
|
||||
addonDiv.innerHTML = `
|
||||
<span>${addon.name} <small class="text-muted">(Required)</small></span>
|
||||
<span class="fw-bold">CHF <span class="addon-price">${addon.price.toFixed(2)}</span></span>
|
||||
`;
|
||||
addonPricingContainer.appendChild(addonDiv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Display selected optional addons
|
||||
if (plan.optional_addons && plan.optional_addons.length > 0) {
|
||||
plan.optional_addons.forEach(addon => {
|
||||
if (selectedAddons.includes(addon.id) && addon.price) {
|
||||
const addonDiv = document.createElement('div');
|
||||
addonDiv.className = 'd-flex justify-content-between mb-2';
|
||||
addonDiv.innerHTML = `
|
||||
<span>${addon.name}</span>
|
||||
<span class="fw-bold">CHF <span class="addon-price">${addon.price.toFixed(2)}</span></span>
|
||||
`;
|
||||
addonPricingContainer.appendChild(addonDiv);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function calculateTotalPrice(plan, storageCost) {
|
||||
let addonTotal = 0;
|
||||
|
||||
// Add mandatory addons
|
||||
if (plan.mandatory_addons) {
|
||||
plan.mandatory_addons.forEach(addon => {
|
||||
if (addon.price) {
|
||||
addonTotal += addon.price;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add selected optional addons
|
||||
if (plan.optional_addons) {
|
||||
plan.optional_addons.forEach(addon => {
|
||||
if (selectedAddons.includes(addon.id) && addon.price) {
|
||||
addonTotal += addon.price;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate final price
|
||||
const finalPrice = plan.final_price + parseFloat(storageCost) + addonTotal;
|
||||
totalPrice.textContent = finalPrice.toFixed(2);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -7,6 +7,53 @@
|
|||
<script src="{% static "js/chart.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.addon-details {
|
||||
max-width: 300px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.addon-item {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.addon-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.addon-price {
|
||||
color: #28a745;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.pricing-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.final-price-header {
|
||||
background-color: #28a745 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.final-price-cell {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.comparison-row {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.servala-row {
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
|
@ -71,6 +118,12 @@
|
|||
Show discount details
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="addon_details" value="true" id="addon_details" {% if show_addon_details %}checked{% endif %}>
|
||||
<label class="form-check-label" for="addon_details">
|
||||
Show addon details
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="price_comparison" value="true" id="price_comparison" {% if show_price_comparison %}checked{% endif %}>
|
||||
<label class="form-check-label" for="price_comparison">
|
||||
|
@ -87,13 +140,16 @@
|
|||
</div>
|
||||
|
||||
<!-- Active Filters Display -->
|
||||
{% if filter_cloud_provider or filter_service or filter_compute_plan_group or filter_service_level %}
|
||||
{% if filter_cloud_provider or filter_service or filter_compute_plan_group or filter_service_level or show_discount_details or show_addon_details or show_price_comparison %}
|
||||
<div class="alert alert-info">
|
||||
<strong>Active Filters:</strong>
|
||||
{% if filter_cloud_provider %}<span class="badge me-1">Cloud Provider: {{ filter_cloud_provider }}</span>{% endif %}
|
||||
{% if filter_service %}<span class="badge me-1">Service: {{ filter_service }}</span>{% endif %}
|
||||
{% if filter_compute_plan_group %}<span class="badge me-1">Group: {{ filter_compute_plan_group }}</span>{% endif %}
|
||||
{% if filter_service_level %}<span class="badge me-1">Service Level: {{ filter_service_level }}</span>{% endif %}
|
||||
{% if show_discount_details %}<span class="badge bg-secondary me-1">Discount Details</span>{% endif %}
|
||||
{% if show_addon_details %}<span class="badge bg-info me-1">Addon Details</span>{% endif %}
|
||||
{% if show_price_comparison %}<span class="badge bg-warning me-1">Price Comparison</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -174,6 +230,52 @@
|
|||
<strong>Replica Enforce:</strong> {{ first_row.replica_enforce }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Display add-on summary #}
|
||||
{% if show_addon_details and first_row.mandatory_addons or first_row.optional_addons %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Available Add-ons for {{ first_row.service }}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if first_row.mandatory_addons %}
|
||||
<div class="mb-3">
|
||||
<h6 class="text-success">Mandatory Add-ons (included in all plans):</h6>
|
||||
<div class="row">
|
||||
{% for addon in first_row.mandatory_addons %}
|
||||
<div class="col-md-4 mb-2">
|
||||
<div class="border border-success rounded p-2">
|
||||
<strong>{{ addon.name }}</strong>
|
||||
<div class="text-muted small">{{ addon.commercial_description|default:addon.description }}</div>
|
||||
<div class="text-success fw-bold">{{ addon.price|floatformat:2 }} {{ first_row.currency }}</div>
|
||||
<small class="text-muted">{{ addon.addon_type }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if first_row.optional_addons %}
|
||||
<div>
|
||||
<h6 class="text-info">Optional Add-ons (can be added):</h6>
|
||||
<div class="row">
|
||||
{% for addon in first_row.optional_addons %}
|
||||
<div class="col-md-4 mb-2">
|
||||
<div class="border border-info rounded p-2">
|
||||
<strong>{{ addon.name }}</strong>
|
||||
<div class="text-muted small">{{ addon.commercial_description|default:addon.description }}</div>
|
||||
<div class="text-info fw-bold">{{ addon.price|floatformat:2 }} {{ first_row.currency }}</div>
|
||||
<small class="text-muted">{{ addon.addon_type }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="table-responsive">
|
||||
|
@ -191,6 +293,9 @@
|
|||
<th>SLA Base</th>
|
||||
<th>SLA Per Unit</th>
|
||||
<th>SLA Price</th>
|
||||
{% if show_addon_details %}
|
||||
<th>Add-ons</th>
|
||||
{% endif %}
|
||||
{% if show_discount_details %}
|
||||
<th>Discount Model</th>
|
||||
<th>Discount Details</th>
|
||||
|
@ -215,6 +320,54 @@
|
|||
<td>{{ row.sla_base|floatformat:2 }}</td>
|
||||
<td>{{ row.sla_per_unit|floatformat:4 }}</td>
|
||||
<td>{{ row.sla_price|floatformat:2 }}</td>
|
||||
{% if show_addon_details %}
|
||||
<td>
|
||||
{% if row.mandatory_addons or row.optional_addons %}
|
||||
<div class="addon-details">
|
||||
{% if row.mandatory_addons %}
|
||||
<div class="mb-2">
|
||||
<small class="text-success fw-bold">Mandatory Add-ons:</small>
|
||||
{% for addon in row.mandatory_addons %}
|
||||
<div class="addon-item border-start border-success ps-2 mb-1">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="addon-name">{{ addon.name }}</span>
|
||||
<span class="addon-price fw-bold">{{ addon.price|floatformat:2 }} {{ row.currency }}</span>
|
||||
</div>
|
||||
{% if addon.commercial_description %}
|
||||
<div class="text-muted small">{{ addon.commercial_description }}</div>
|
||||
{% elif addon.description %}
|
||||
<div class="text-muted small">{{ addon.description }}</div>
|
||||
{% endif %}
|
||||
<div class="text-muted small">Type: {{ addon.addon_type }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if row.optional_addons %}
|
||||
<div>
|
||||
<small class="text-info fw-bold">Optional Add-ons:</small>
|
||||
{% for addon in row.optional_addons %}
|
||||
<div class="addon-item border-start border-info ps-2 mb-1">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="addon-name">{{ addon.name }}</span>
|
||||
<span class="addon-price">{{ addon.price|floatformat:2 }} {{ row.currency }}</span>
|
||||
</div>
|
||||
{% if addon.commercial_description %}
|
||||
<div class="text-muted small">{{ addon.commercial_description }}</div>
|
||||
{% elif addon.description %}
|
||||
<div class="text-muted small">{{ addon.description }}</div>
|
||||
{% endif %}
|
||||
<div class="text-muted small">Type: {{ addon.addon_type }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted">No add-ons</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if show_discount_details %}
|
||||
<td>
|
||||
{% if row.has_discount %}
|
||||
|
@ -267,6 +420,9 @@
|
|||
<td class="text-muted">-</td>
|
||||
<td class="text-muted">-</td>
|
||||
<td class="text-muted">-</td>
|
||||
{% if show_addon_details %}
|
||||
<td class="text-muted">-</td>
|
||||
{% endif %}
|
||||
{% if show_discount_details %}
|
||||
<td class="text-muted">-</td>
|
||||
<td class="text-muted">-</td>
|
||||
|
@ -338,6 +494,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const filterForm = document.getElementById('filter-form');
|
||||
const filterSelects = document.querySelectorAll('.filter-select');
|
||||
const discountCheckbox = document.getElementById('discount_details');
|
||||
const addonCheckbox = document.getElementById('addon_details');
|
||||
|
||||
// Add change event listeners to all filter dropdowns
|
||||
filterSelects.forEach(function(select) {
|
||||
|
@ -351,6 +508,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
filterForm.submit();
|
||||
});
|
||||
|
||||
// Add change event listener to addon details checkbox
|
||||
addonCheckbox.addEventListener('change', function() {
|
||||
filterForm.submit();
|
||||
});
|
||||
|
||||
// Add change event listener to price comparison checkbox
|
||||
const priceComparisonCheckbox = document.getElementById('price_comparison');
|
||||
priceComparisonCheckbox.addEventListener('change', function() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue