[FIX] website_sale_aplicoop: Complete infinite scroll and search filter integration

Major fixes:
- Fix JSON body parsing in load_products_ajax with type='http' route
  * Parse JSON from request.httprequest.get_data() instead of post params
  * Correctly read page, search, category from JSON request body

- Fix search and category filter combination
  * Use intersection (&) instead of replacement to preserve both filters
  * Now respects search AND category simultaneously

- Integrate realtime_search.js with infinite_scroll.js
  * Add resetWithFilters() method to reset scroll to page 1 with new filters
  * When search/category changes, reload products from server
  * Clear grid and load fresh results

- Fix pagination reset logic
  * Set currentPage = 0 in resetWithFilters() so loadNextPage() increments to 1
  * Prevents loading empty page 2 when resetting filters

Results:
 Infinite scroll loads all pages correctly (1, 2, 3...)
 Search filters work across all products (not just loaded)
 Category filters work correctly
 Search AND category filters work together
 Page resets to 1 when filters change
This commit is contained in:
snt 2026-02-17 01:26:20 +01:00
parent 5eb039ffe0
commit 40ce973bd6
4 changed files with 463 additions and 239 deletions

View file

@ -1380,7 +1380,7 @@ class AplicoopWebsiteSale(WebsiteSale):
@http.route(
["/eskaera/<int:order_id>/load-products-ajax"],
type="json",
type="http",
auth="user",
website=True,
methods=["POST"],
@ -1407,17 +1407,27 @@ class AplicoopWebsiteSale(WebsiteSale):
)
)
# Get page from POST
# Parse JSON body for parameters (type="http" doesn't auto-parse JSON)
params = {}
try:
page = int(post.get("page", 1))
if request.httprequest.content_length:
data = request.httprequest.get_data(as_text=True)
if data:
params = json.loads(data)
except (ValueError, json.JSONDecodeError, AttributeError):
params = {}
# Get page from POST/JSON
try:
page = int(params.get("page", post.get("page", 1)))
if page < 1:
page = 1
except (ValueError, TypeError):
page = 1
# Get filters
search_query = post.get("search", "").strip()
category_filter = str(post.get("category", "0"))
search_query = params.get("search", post.get("search", "")).strip()
category_filter = str(params.get("category", post.get("category", "0")))
_logger.info(
"load_products_ajax: order_id=%d, page=%d, search=%s, category=%s",
@ -1477,7 +1487,8 @@ class AplicoopWebsiteSale(WebsiteSale):
lambda p: p.categ_id.id in order_cat_ids
)
filtered_products = cat_filtered
# Preserve search filter by using intersection
filtered_products = filtered_products & cat_filtered
except (ValueError, TypeError) as e:
_logger.warning(
"load_products_ajax: Invalid category filter: %s", str(e)
@ -1556,7 +1567,7 @@ class AplicoopWebsiteSale(WebsiteSale):
}
# Render HTML
html = request.env["ir.ui.view"]._render(
html = request.env["ir.ui.view"]._render_template(
"website_sale_aplicoop.eskaera_shop_products",
{
"group_order": group_order,
@ -1571,13 +1582,18 @@ class AplicoopWebsiteSale(WebsiteSale):
},
)
return {
return request.make_response(
json.dumps(
{
"html": html,
"has_next": has_next,
"next_page": page + 1,
"total": total_products,
"page": page,
}
),
[("Content-Type", "application/json")],
)
@http.route(
["/eskaera/add-to-cart"],

View file

@ -7,6 +7,26 @@
console.log("[INFINITE_SCROLL] Script loaded!");
// Visual indicator for debugging
if (typeof document !== "undefined") {
try {
var debugDiv = document.createElement("div");
debugDiv.innerHTML = "[INFINITE_SCROLL LOADED]";
debugDiv.style.position = "fixed";
debugDiv.style.top = "0";
debugDiv.style.right = "0";
debugDiv.style.backgroundColor = "#00ff00";
debugDiv.style.color = "#000";
debugDiv.style.padding = "5px 10px";
debugDiv.style.fontSize = "12px";
debugDiv.style.zIndex = "99999";
debugDiv.id = "infinite-scroll-debug";
document.body.appendChild(debugDiv);
} catch (e) {
console.error("[INFINITE_SCROLL] Error adding debug div:", e);
}
}
(function () {
"use strict";
@ -38,12 +58,14 @@ console.log("[INFINITE_SCROLL] Script loaded!");
this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20;
this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1;
// Check if there are more products to load
var hasNextBtn = document.getElementById("load-more-btn");
this.hasMore = hasNextBtn && hasNextBtn.offsetParent !== null; // offsetParent === null means hidden
// Check if there are more products to load from data attribute
var hasNextAttr = configEl.getAttribute("data-has-next");
this.hasMore = hasNextAttr === "true" || hasNextAttr === "True";
if (!this.hasMore) {
console.log("[INFINITE_SCROLL] No more products to load");
console.log(
"[INFINITE_SCROLL] No more products to load (has_next=" + hasNextAttr + ")"
);
return;
}
@ -108,6 +130,44 @@ console.log("[INFINITE_SCROLL] Script loaded!");
console.log("[INFINITE_SCROLL] Fallback button listener attached");
},
resetWithFilters: function (searchQuery, categoryId) {
/**
* Reset infinite scroll to page 1 with new filters and reload products.
* Called by realtime_search when filters change.
*/
console.log(
"[INFINITE_SCROLL] Resetting with filters: search=" +
searchQuery +
" category=" +
categoryId
);
this.searchQuery = searchQuery || "";
this.category = categoryId || "0";
this.currentPage = 0; // Set to 0 so loadNextPage() increments to 1
this.isLoading = false;
this.hasMore = true;
// Update the config element data attributes for consistency
var configEl = document.getElementById("eskaera-config");
if (configEl) {
configEl.setAttribute("data-search", this.searchQuery);
configEl.setAttribute("data-category", this.category);
configEl.setAttribute("data-current-page", "1");
configEl.setAttribute("data-has-next", "true");
}
// Clear the grid and reload from page 1
var grid = document.getElementById("products-grid");
if (grid) {
grid.innerHTML = "";
console.log("[INFINITE_SCROLL] Grid cleared");
}
// Load first page with new filters
this.loadNextPage();
},
loadNextPage: function () {
var self = this;
this.isLoading = true;

View file

@ -4,7 +4,7 @@
*/
(function () {
'use strict';
"use strict";
window.realtimeSearch = {
searchInput: null,
@ -17,27 +17,29 @@
availableTags: {}, // Maps tag ID to {id, name, count}
init: function () {
console.log('[realtimeSearch] Initializing...');
console.log("[realtimeSearch] Initializing...");
// searchInput y categorySelect ya fueron asignados por tryInit()
console.log('[realtimeSearch] Search input:', this.searchInput);
console.log('[realtimeSearch] Category select:', this.categorySelect);
console.log("[realtimeSearch] Search input:", this.searchInput);
console.log("[realtimeSearch] Category select:", this.categorySelect);
if (!this.searchInput) {
console.error('[realtimeSearch] ERROR: Search input not found!');
console.error("[realtimeSearch] ERROR: Search input not found!");
return false;
}
if (!this.categorySelect) {
console.error('[realtimeSearch] ERROR: Category select not found!');
console.error("[realtimeSearch] ERROR: Category select not found!");
return false;
}
this._buildCategoryHierarchyFromDOM();
this._storeAllProducts();
console.log('[realtimeSearch] After _storeAllProducts(), calling _attachEventListeners()...');
console.log(
"[realtimeSearch] After _storeAllProducts(), calling _attachEventListeners()..."
);
this._attachEventListeners();
console.log('[realtimeSearch] ✓ Initialized successfully');
console.log("[realtimeSearch] ✓ Initialized successfully");
return true;
},
@ -50,11 +52,11 @@
* Estructura: categoryHierarchy[parentCategoryId] = [childCategoryId1, childCategoryId2, ...]
*/
var self = this;
var allOptions = this.categorySelect.querySelectorAll('option[value]');
var allOptions = this.categorySelect.querySelectorAll("option[value]");
var optionStack = []; // Stack para mantener los padres en cada nivel
allOptions.forEach(function (option) {
var categoryId = option.getAttribute('value');
var categoryId = option.getAttribute("value");
var text = option.textContent;
// Contar arrows para determinar profundidad
@ -87,27 +89,33 @@
}
});
console.log('[realtimeSearch] Complete category hierarchy built:', self.categoryHierarchy);
console.log(
"[realtimeSearch] Complete category hierarchy built:",
self.categoryHierarchy
);
},
_storeAllProducts: function () {
var productCards = document.querySelectorAll('.product-card');
console.log('[realtimeSearch] Found ' + productCards.length + ' product cards');
var productCards = document.querySelectorAll(".product-card");
console.log("[realtimeSearch] Found " + productCards.length + " product cards");
var self = this;
this.allProducts = [];
productCards.forEach(function (card, index) {
var name = card.getAttribute('data-product-name') || '';
var categoryId = card.getAttribute('data-category-id') || '';
var tagIdsStr = card.getAttribute('data-product-tags') || '';
var name = card.getAttribute("data-product-name") || "";
var categoryId = card.getAttribute("data-category-id") || "";
var tagIdsStr = card.getAttribute("data-product-tags") || "";
// Parse tag IDs from comma-separated string
var tagIds = [];
if (tagIdsStr) {
tagIds = tagIdsStr.split(',').map(function(id) {
tagIds = tagIdsStr
.split(",")
.map(function (id) {
return parseInt(id.trim(), 10);
}).filter(function(id) {
})
.filter(function (id) {
return !isNaN(id);
});
}
@ -117,11 +125,11 @@
name: name.toLowerCase(),
category: categoryId.toString(),
originalCategory: categoryId,
tags: tagIds // Array of tag IDs for this product
tags: tagIds, // Array of tag IDs for this product
});
});
console.log('[realtimeSearch] Total products stored: ' + this.allProducts.length);
console.log("[realtimeSearch] Total products stored: " + this.allProducts.length);
},
_attachEventListeners: function () {
@ -134,131 +142,175 @@
self.originalTagColors = {}; // Maps tag ID to original color
// Store last values at instance level so polling can access them
self.lastSearchValue = '';
self.lastCategoryValue = '';
self.lastSearchValue = "";
self.lastCategoryValue = "";
// Prevent form submission completely
var form = self.searchInput.closest('form');
var form = self.searchInput.closest("form");
if (form) {
form.addEventListener('submit', function(e) {
form.addEventListener("submit", function (e) {
e.preventDefault();
e.stopPropagation();
console.log('[realtimeSearch] Form submission prevented and stopped');
console.log("[realtimeSearch] Form submission prevented and stopped");
return false;
});
}
// Prevent Enter key from submitting
self.searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
self.searchInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
console.log('[realtimeSearch] Enter key prevented on search input');
console.log("[realtimeSearch] Enter key prevented on search input");
return false;
}
});
// Search input: listen to 'input' for real-time filtering
self.searchInput.addEventListener('input', function(e) {
self.searchInput.addEventListener("input", function (e) {
try {
console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"');
self._filterProducts();
} catch (error) {
console.error('[realtimeSearch] Error in input listener:', error.message);
console.error("[realtimeSearch] Error in input listener:", error.message);
}
});
// Also keep 'keyup' for extra compatibility
self.searchInput.addEventListener('keyup', function(e) {
self.searchInput.addEventListener("keyup", function (e) {
try {
console.log('[realtimeSearch] KEYUP event - value: "' + e.target.value + '"');
self._filterProducts();
} catch (error) {
console.error('[realtimeSearch] Error in keyup listener:', error.message);
console.error("[realtimeSearch] Error in keyup listener:", error.message);
}
});
// Category select
self.categorySelect.addEventListener('change', function(e) {
self.categorySelect.addEventListener("change", function (e) {
try {
console.log('[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"');
console.log(
'[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"'
);
self._filterProducts();
} catch (error) {
console.error('[realtimeSearch] Error in category change listener:', error.message);
console.error(
"[realtimeSearch] Error in category change listener:",
error.message
);
}
});
// Tag filter badges: click to toggle selection (independent state)
var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
console.log('[realtimeSearch] Found ' + tagBadges.length + ' tag filter badges');
console.log("[realtimeSearch] Found " + tagBadges.length + " tag filter badges");
// Get theme colors from CSS variables
var rootStyles = getComputedStyle(document.documentElement);
var primaryColor = rootStyles.getPropertyValue('--bs-primary').trim() ||
rootStyles.getPropertyValue('--primary').trim() ||
'#0d6efd';
var secondaryColor = rootStyles.getPropertyValue('--bs-secondary').trim() ||
rootStyles.getPropertyValue('--secondary').trim() ||
'#6c757d';
var primaryColor =
rootStyles.getPropertyValue("--bs-primary").trim() ||
rootStyles.getPropertyValue("--primary").trim() ||
"#0d6efd";
var secondaryColor =
rootStyles.getPropertyValue("--bs-secondary").trim() ||
rootStyles.getPropertyValue("--secondary").trim() ||
"#6c757d";
console.log('[realtimeSearch] Theme colors - Primary:', primaryColor, 'Secondary:', secondaryColor);
console.log(
"[realtimeSearch] Theme colors - Primary:",
primaryColor,
"Secondary:",
secondaryColor
);
// Store original colors for each badge BEFORE adding event listeners
tagBadges.forEach(function (badge) {
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
var tagColor = badge.getAttribute('data-tag-color');
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var tagColor = badge.getAttribute("data-tag-color");
// Store the original color (either from data-tag-color or use secondary for tags without color)
if (tagColor) {
self.originalTagColors[tagId] = tagColor;
console.log('[realtimeSearch] Stored original color for tag ' + tagId + ': ' + tagColor);
console.log(
"[realtimeSearch] Stored original color for tag " + tagId + ": " + tagColor
);
} else {
self.originalTagColors[tagId] = 'var(--bs-secondary, ' + secondaryColor + ')';
console.log('[realtimeSearch] Tag ' + tagId + ' has no color, using secondary');
self.originalTagColors[tagId] = "var(--bs-secondary, " + secondaryColor + ")";
console.log("[realtimeSearch] Tag " + tagId + " has no color, using secondary");
}
});
tagBadges.forEach(function (badge) {
badge.addEventListener('click', function(e) {
badge.addEventListener("click", function (e) {
e.preventDefault();
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var originalColor = self.originalTagColors[tagId];
console.log('[realtimeSearch] Tag badge clicked: ' + tagId + ' (original color: ' + originalColor + ')');
console.log(
"[realtimeSearch] Tag badge clicked: " +
tagId +
" (original color: " +
originalColor +
")"
);
// Toggle tag selection
if (self.selectedTags.has(tagId)) {
// Deselect
self.selectedTags.delete(tagId);
console.log('[realtimeSearch] Tag ' + tagId + ' deselected');
console.log("[realtimeSearch] Tag " + tagId + " deselected");
} else {
// Select
self.selectedTags.add(tagId);
console.log('[realtimeSearch] Tag ' + tagId + ' selected');
console.log("[realtimeSearch] Tag " + tagId + " selected");
}
// Update colors for ALL badges based on selection state
tagBadges.forEach(function (badge) {
var id = parseInt(badge.getAttribute('data-tag-id'), 10);
var id = parseInt(badge.getAttribute("data-tag-id"), 10);
if (self.selectedTags.size === 0) {
// No tags selected: restore all to original colors
var originalColor = self.originalTagColors[id];
badge.style.setProperty('background-color', originalColor, 'important');
badge.style.setProperty('border-color', originalColor, 'important');
badge.style.setProperty('color', '#ffffff', 'important');
console.log('[realtimeSearch] Badge ' + id + ' reset to original color (no selection)');
badge.style.setProperty("background-color", originalColor, "important");
badge.style.setProperty("border-color", originalColor, "important");
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " +
id +
" reset to original color (no selection)"
);
} else if (self.selectedTags.has(id)) {
// Selected: primary color
badge.style.setProperty('background-color', 'var(--bs-primary, ' + primaryColor + ')', 'important');
badge.style.setProperty('border-color', 'var(--bs-primary, ' + primaryColor + ')', 'important');
badge.style.setProperty('color', '#ffffff', 'important');
console.log('[realtimeSearch] Badge ' + id + ' set to primary (selected)');
badge.style.setProperty(
"background-color",
"var(--bs-primary, " + primaryColor + ")",
"important"
);
badge.style.setProperty(
"border-color",
"var(--bs-primary, " + primaryColor + ")",
"important"
);
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " + id + " set to primary (selected)"
);
} else {
// Not selected but others are: secondary color
badge.style.setProperty('background-color', 'var(--bs-secondary, ' + secondaryColor + ')', 'important');
badge.style.setProperty('border-color', 'var(--bs-secondary, ' + secondaryColor + ')', 'important');
badge.style.setProperty('color', '#ffffff', 'important');
console.log('[realtimeSearch] Badge ' + id + ' set to secondary (not selected)');
badge.style.setProperty(
"background-color",
"var(--bs-secondary, " + secondaryColor + ")",
"important"
);
badge.style.setProperty(
"border-color",
"var(--bs-secondary, " + secondaryColor + ")",
"important"
);
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " + id + " set to secondary (not selected)"
);
}
});
@ -269,9 +321,9 @@
// POLLING FALLBACK: Since Odoo components may intercept events,
// use polling to detect value changes
console.log('[realtimeSearch] 🚀 POLLING INITIALIZATION STARTING');
console.log('[realtimeSearch] Search input element:', self.searchInput);
console.log('[realtimeSearch] Category select element:', self.categorySelect);
console.log("[realtimeSearch] 🚀 POLLING INITIALIZATION STARTING");
console.log("[realtimeSearch] Search input element:", self.searchInput);
console.log("[realtimeSearch] Category select element:", self.categorySelect);
var pollingCounter = 0;
var pollInterval = setInterval(function () {
@ -279,47 +331,107 @@
pollingCounter++;
// Try multiple ways to get the search value
var currentSearchValue = self.searchInput.value || '';
var currentSearchAttr = self.searchInput.getAttribute('value') || '';
var currentSearchDataValue = self.searchInput.getAttribute('data-value') || '';
var currentSearchInnerText = self.searchInput.innerText || '';
var currentSearchValue = self.searchInput.value || "";
var currentSearchAttr = self.searchInput.getAttribute("value") || "";
var currentSearchDataValue = self.searchInput.getAttribute("data-value") || "";
var currentSearchInnerText = self.searchInput.innerText || "";
var currentCategoryValue = self.categorySelect ? (self.categorySelect.value || '') : '';
var currentCategoryValue = self.categorySelect
? self.categorySelect.value || ""
: "";
// FIRST POLL: Detailed debug
if (pollingCounter === 1) {
console.log('═══════════════════════════════════════════');
console.log('[realtimeSearch] 🔍 FIRST POLLING DEBUG (POLL #1)');
console.log('═══════════════════════════════════════════');
console.log('Search input .value:', JSON.stringify(currentSearchValue));
console.log('Search input getAttribute("value"):', JSON.stringify(currentSearchAttr));
console.log('Search input getAttribute("data-value"):', JSON.stringify(currentSearchDataValue));
console.log('Search input innerText:', JSON.stringify(currentSearchInnerText));
console.log('Category select .value:', JSON.stringify(currentCategoryValue));
console.log('Last stored values - search:"' + self.lastSearchValue + '" category:"' + self.lastCategoryValue + '"');
console.log('═══════════════════════════════════════════');
console.log("═══════════════════════════════════════════");
console.log("[realtimeSearch] 🔍 FIRST POLLING DEBUG (POLL #1)");
console.log("═══════════════════════════════════════════");
console.log("Search input .value:", JSON.stringify(currentSearchValue));
console.log(
'Search input getAttribute("value"):',
JSON.stringify(currentSearchAttr)
);
console.log(
'Search input getAttribute("data-value"):',
JSON.stringify(currentSearchDataValue)
);
console.log(
"Search input innerText:",
JSON.stringify(currentSearchInnerText)
);
console.log(
"Category select .value:",
JSON.stringify(currentCategoryValue)
);
console.log(
'Last stored values - search:"' +
self.lastSearchValue +
'" category:"' +
self.lastCategoryValue +
'"'
);
console.log("═══════════════════════════════════════════");
}
// Log every 20 polls (reduce spam)
if (pollingCounter % 20 === 0) {
console.log('[realtimeSearch] POLLING #' + pollingCounter + ': search="' + currentSearchValue + '" category="' + currentCategoryValue + '"');
console.log(
"[realtimeSearch] POLLING #" +
pollingCounter +
': search="' +
currentSearchValue +
'" category="' +
currentCategoryValue +
'"'
);
}
// Check for ANY change in either field
if (currentSearchValue !== self.lastSearchValue || currentCategoryValue !== self.lastCategoryValue) {
console.log('[realtimeSearch] ⚡ CHANGE DETECTED: search="' + currentSearchValue + '" (was:"' + self.lastSearchValue + '") | category="' + currentCategoryValue + '" (was:"' + self.lastCategoryValue + '")');
if (
currentSearchValue !== self.lastSearchValue ||
currentCategoryValue !== self.lastCategoryValue
) {
console.log(
'[realtimeSearch] ⚡ CHANGE DETECTED: search="' +
currentSearchValue +
'" (was:"' +
self.lastSearchValue +
'") | category="' +
currentCategoryValue +
'" (was:"' +
self.lastCategoryValue +
'")'
);
self.lastSearchValue = currentSearchValue;
self.lastCategoryValue = currentCategoryValue;
// Reset infinite scroll with new filters (will reload from server)
if (
window.infiniteScroll &&
typeof window.infiniteScroll.resetWithFilters === "function"
) {
console.log(
"[realtimeSearch] Calling infiniteScroll.resetWithFilters()"
);
window.infiniteScroll.resetWithFilters(
currentSearchValue,
currentCategoryValue
);
} else {
// Fallback: filter locally (but this only filters loaded products)
console.log(
"[realtimeSearch] infiniteScroll not available, filtering locally only"
);
self._filterProducts();
}
}
} catch (error) {
console.error('[realtimeSearch] ❌ Error in polling:', error.message);
console.error("[realtimeSearch] ❌ Error in polling:", error.message);
}
}, 300); // Check every 300ms
console.log('[realtimeSearch] ✅ Polling interval started with ID:', pollInterval);
console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval);
console.log('[realtimeSearch] Event listeners attached with polling fallback');
console.log("[realtimeSearch] Event listeners attached with polling fallback");
},
_initializeAvailableTags: function () {
@ -331,29 +443,40 @@
var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
tagBadges.forEach(function (badge) {
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
var tagName = badge.getAttribute('data-tag-name') || '';
var countSpan = badge.querySelector('.tag-count');
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var tagName = badge.getAttribute("data-tag-name") || "";
var countSpan = badge.querySelector(".tag-count");
var count = countSpan ? parseInt(countSpan.textContent, 10) : 0;
self.availableTags[tagId] = {
id: tagId,
name: tagName,
count: count
count: count,
};
});
console.log('[realtimeSearch] Initialized ' + Object.keys(self.availableTags).length + ' available tags');
console.log(
"[realtimeSearch] Initialized " +
Object.keys(self.availableTags).length +
" available tags"
);
},
_filterProducts: function () {
var self = this;
try {
var searchQuery = (self.searchInput.value || '').toLowerCase().trim();
var selectedCategoryId = (self.categorySelect.value || '').toString();
var searchQuery = (self.searchInput.value || "").toLowerCase().trim();
var selectedCategoryId = (self.categorySelect.value || "").toString();
console.log('[realtimeSearch] Filtering: search=' + searchQuery + ' category=' + selectedCategoryId + ' tags=' + Array.from(self.selectedTags).join(','));
console.log(
"[realtimeSearch] Filtering: search=" +
searchQuery +
" category=" +
selectedCategoryId +
" tags=" +
Array.from(self.selectedTags).join(",")
);
// Build a set of allowed category IDs (selected category + ALL descendants recursively)
var allowedCategories = {};
@ -377,8 +500,17 @@
};
var allDescendants = getAllDescendants(selectedCategoryId);
console.log('[realtimeSearch] Selected category ' + selectedCategoryId + ' has ' + allDescendants.length + ' total descendants');
console.log('[realtimeSearch] Allowed categories:', Object.keys(allowedCategories));
console.log(
"[realtimeSearch] Selected category " +
selectedCategoryId +
" has " +
allDescendants.length +
" total descendants"
);
console.log(
"[realtimeSearch] Allowed categories:",
Object.keys(allowedCategories)
);
}
var visibleCount = 0;
@ -392,7 +524,8 @@
self.allProducts.forEach(function (product) {
var nameMatches = !searchQuery || product.name.indexOf(searchQuery) !== -1;
var categoryMatches = !selectedCategoryId || allowedCategories[product.category];
var categoryMatches =
!selectedCategoryId || allowedCategories[product.category];
// Tag filtering: if tags are selected, product must have AT LEAST ONE selected tag (OR logic)
var tagMatches = true;
@ -405,7 +538,7 @@
var shouldShow = nameMatches && categoryMatches && tagMatches;
if (shouldShow) {
product.element.classList.remove('hidden-product');
product.element.classList.remove("hidden-product");
visibleCount++;
// Count this product's tags toward the dynamic counters
@ -415,7 +548,7 @@
}
});
} else {
product.element.classList.add('hidden-product');
product.element.classList.add("hidden-product");
hiddenCount++;
}
});
@ -424,71 +557,85 @@
for (var tagId in tagCounts) {
var badge = document.querySelector('[data-tag-id="' + tagId + '"]');
if (badge) {
var countSpan = badge.querySelector('.tag-count');
var countSpan = badge.querySelector(".tag-count");
if (countSpan) {
countSpan.textContent = tagCounts[tagId];
}
}
}
console.log('[realtimeSearch] Filter result: visible=' + visibleCount + ' hidden=' + hiddenCount);
console.log(
"[realtimeSearch] Filter result: visible=" +
visibleCount +
" hidden=" +
hiddenCount
);
} catch (error) {
console.error('[realtimeSearch] ERROR in _filterProducts():', error.message);
console.error('[realtimeSearch] Stack:', error.stack);
}
console.error("[realtimeSearch] ERROR in _filterProducts():", error.message);
console.error("[realtimeSearch] Stack:", error.stack);
}
},
};
// Initialize when DOM is ready
console.log('[realtimeSearch] Script loaded, DOM state: ' + document.readyState);
console.log("[realtimeSearch] Script loaded, DOM state: " + document.readyState);
function tryInit() {
try {
console.log('[realtimeSearch] Attempting initialization...');
console.log("[realtimeSearch] Attempting initialization...");
// Query product cards
var productCards = document.querySelectorAll('.product-card');
console.log('[realtimeSearch] Found ' + productCards.length + ' product cards');
var productCards = document.querySelectorAll(".product-card");
console.log("[realtimeSearch] Found " + productCards.length + " product cards");
// Use the NEW pure HTML input with ID (not transformed by Odoo)
var searchInput = document.getElementById('realtime-search-input');
console.log('[realtimeSearch] Search input found:', !!searchInput);
var searchInput = document.getElementById("realtime-search-input");
console.log("[realtimeSearch] Search input found:", !!searchInput);
if (searchInput) {
console.log('[realtimeSearch] Search input class:', searchInput.className);
console.log('[realtimeSearch] Search input type:', searchInput.type);
console.log("[realtimeSearch] Search input class:", searchInput.className);
console.log("[realtimeSearch] Search input type:", searchInput.type);
}
// Category select with ID (not transformed by Odoo)
var categorySelect = document.getElementById('realtime-category-select');
console.log('[realtimeSearch] Category select found:', !!categorySelect);
var categorySelect = document.getElementById("realtime-category-select");
console.log("[realtimeSearch] Category select found:", !!categorySelect);
if (productCards.length > 0 && searchInput) {
console.log('[realtimeSearch] ✓ All elements found! Initializing...');
console.log("[realtimeSearch] ✓ All elements found! Initializing...");
// Assign elements to window.realtimeSearch BEFORE calling init()
window.realtimeSearch.searchInput = searchInput;
window.realtimeSearch.categorySelect = categorySelect;
window.realtimeSearch.init();
console.log('[realtimeSearch] ✓ Initialization complete!');
console.log("[realtimeSearch] ✓ Initialization complete!");
} else {
console.log('[realtimeSearch] Waiting for elements... (products:' + productCards.length + ', search:' + !!searchInput + ')');
console.log(
"[realtimeSearch] Waiting for elements... (products:" +
productCards.length +
", search:" +
!!searchInput +
")"
);
if (productCards.length === 0) {
console.log('[realtimeSearch] No product cards found. Current HTML body length:', document.body.innerHTML.length);
console.log(
"[realtimeSearch] No product cards found. Current HTML body length:",
document.body.innerHTML.length
);
}
setTimeout(tryInit, 500);
}
} catch (error) {
console.error('[realtimeSearch] ERROR in tryInit():', error.message);
console.error("[realtimeSearch] ERROR in tryInit():", error.message);
}
}
if (document.readyState === 'loading') {
console.log('[realtimeSearch] Adding DOMContentLoaded listener');
document.addEventListener('DOMContentLoaded', function() {
console.log('[realtimeSearch] DOMContentLoaded fired');
if (document.readyState === "loading") {
console.log("[realtimeSearch] Adding DOMContentLoaded listener");
document.addEventListener("DOMContentLoaded", function () {
console.log("[realtimeSearch] DOMContentLoaded fired");
tryInit();
});
} else {
console.log('[realtimeSearch] DOM already loaded, initializing with delay');
console.log("[realtimeSearch] DOM already loaded, initializing with delay");
setTimeout(tryInit, 500);
}
})();

View file

@ -586,6 +586,7 @@
t-attf-data-category="{{ selected_category }}"
t-attf-data-per-page="{{ per_page }}"
t-attf-data-current-page="{{ current_page }}"
t-attf-data-has-next="{{ 'true' if has_next else 'false' }}"
class="d-none">
</div>
</t>