From 40ce973bd6ccd05257517b10244157ba061918f5 Mon Sep 17 00:00:00 2001 From: snt Date: Tue, 17 Feb 2026 01:26:20 +0100 Subject: [PATCH] [FIX] website_sale_aplicoop: Complete infinite scroll and search filter integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../controllers/website_sale.py | 44 +- .../static/src/js/infinite_scroll.js | 68 ++- .../static/src/js/realtime_search.js | 563 +++++++++++------- .../views/website_templates.xml | 27 +- 4 files changed, 463 insertions(+), 239 deletions(-) diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 14b3f91..2ca0776 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -1380,7 +1380,7 @@ class AplicoopWebsiteSale(WebsiteSale): @http.route( ["/eskaera//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 { - "html": html, - "has_next": has_next, - "next_page": page + 1, - "total": total_products, - "page": page, - } + 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"], diff --git a/website_sale_aplicoop/static/src/js/infinite_scroll.js b/website_sale_aplicoop/static/src/js/infinite_scroll.js index 99b7723..65b6f35 100644 --- a/website_sale_aplicoop/static/src/js/infinite_scroll.js +++ b/website_sale_aplicoop/static/src/js/infinite_scroll.js @@ -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; diff --git a/website_sale_aplicoop/static/src/js/realtime_search.js b/website_sale_aplicoop/static/src/js/realtime_search.js index a8da9c8..71fd4cb 100644 --- a/website_sale_aplicoop/static/src/js/realtime_search.js +++ b/website_sale_aplicoop/static/src/js/realtime_search.js @@ -3,8 +3,8 @@ * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) */ -(function() { - 'use strict'; +(function () { + "use strict"; window.realtimeSearch = { searchInput: null, @@ -16,57 +16,59 @@ selectedTags: new Set(), // Set of selected tag IDs (for OR logic filtering) availableTags: {}, // Maps tag ID to {id, name, count} - init: function() { - console.log('[realtimeSearch] Initializing...'); - + init: function () { + 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; }, - _buildCategoryHierarchyFromDOM: function() { + _buildCategoryHierarchyFromDOM: function () { /** * Construye un mapa de jerarquía de categorías desde las opciones del select. * Ahora todas las opciones son planas pero con indentación visual (↳ arrows). - * + * * La profundidad se determina contando el número de arrows (↳). * 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'); + + allOptions.forEach(function (option) { + var categoryId = option.getAttribute("value"); var text = option.textContent; - + // Contar arrows para determinar profundidad var arrowCount = (text.match(/↳/g) || []).length; var depth = arrowCount; // Depth: 0 for roots, 1 for children, 2 for grandchildren, etc. - + // Ajustar el stack al nivel actual // Si la profundidad es menor o igual, sacamos elementos del stack while (optionStack.length > depth) { optionStack.pop(); } - + // Si hay un padre en el stack (profundidad > 0), agregar como hijo if (depth > 0 && optionStack.length > 0) { var parentId = optionStack[optionStack.length - 1]; @@ -77,7 +79,7 @@ self.categoryHierarchy[parentId].push(categoryId); } } - + // Agregar este ID al stack como posible padre para los siguientes // Adjust position in stack based on depth if (optionStack.length > depth) { @@ -86,286 +88,407 @@ optionStack.push(categoryId); } }); - - 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'); - + _storeAllProducts: function () { + 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') || ''; - + + 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") || ""; + // Parse tag IDs from comma-separated string var tagIds = []; if (tagIdsStr) { - tagIds = tagIdsStr.split(',').map(function(id) { - return parseInt(id.trim(), 10); - }).filter(function(id) { - return !isNaN(id); - }); + tagIds = tagIdsStr + .split(",") + .map(function (id) { + return parseInt(id.trim(), 10); + }) + .filter(function (id) { + return !isNaN(id); + }); } - + self.allProducts.push({ element: card, 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() { + _attachEventListeners: function () { var self = this; - + // Initialize available tags from DOM self._initializeAvailableTags(); - + // Store original colors for each tag badge 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'; - - console.log('[realtimeSearch] Theme colors - Primary:', primaryColor, 'Secondary:', secondaryColor); - + 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 + ); + // 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'); - + tagBadges.forEach(function (badge) { + 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) { + + tagBadges.forEach(function (badge) { + 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); - + tagBadges.forEach(function (badge) { + 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)" + ); } }); - + // Filter products (independent of search/category state) self._filterProducts(); }); }); - + // 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() { + var pollInterval = setInterval(function () { try { 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 currentCategoryValue = self.categorySelect ? (self.categorySelect.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 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; - self._filterProducts(); + + // 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] Event listeners attached with polling fallback'); + }, 300); // Check every 300ms + + console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval); + + console.log("[realtimeSearch] Event listeners attached with polling fallback"); }, - - _initializeAvailableTags: function() { + + _initializeAvailableTags: function () { /** * Initialize availableTags map from the DOM tag filter badges. * Format: availableTags[tagId] = {id, name, count} */ var self = this; 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'); + + 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 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() { + _filterProducts: function () { var self = this; try { - 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(',')); + 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(",") + ); // Build a set of allowed category IDs (selected category + ALL descendants recursively) var allowedCategories = {}; - + if (selectedCategoryId) { allowedCategories[selectedCategoryId] = true; - + // Recursive function to get all descendants - var getAllDescendants = function(parentId) { + var getAllDescendants = function (parentId) { var descendants = []; if (self.categoryHierarchy[parentId]) { - self.categoryHierarchy[parentId].forEach(function(childId) { + self.categoryHierarchy[parentId].forEach(function (childId) { descendants.push(childId); allowedCategories[childId] = true; // Recursivamente obtener descendientes del hijo @@ -375,47 +498,57 @@ } return descendants; }; - + 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; var hiddenCount = 0; - + // Track tag counts for dynamic badge updates var tagCounts = {}; for (var tagId in self.availableTags) { tagCounts[tagId] = 0; } - self.allProducts.forEach(function(product) { + 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; if (self.selectedTags.size > 0) { - tagMatches = product.tags.some(function(productTagId) { + tagMatches = product.tags.some(function (productTagId) { return self.selectedTags.has(productTagId); }); } - + 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 - product.tags.forEach(function(tagId) { + product.tags.forEach(function (tagId) { if (tagCounts.hasOwnProperty(tagId)) { tagCounts[tagId]++; } }); } 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); } })(); diff --git a/website_sale_aplicoop/views/website_templates.xml b/website_sale_aplicoop/views/website_templates.xml index ce07ce2..1d135ba 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -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"> @@ -656,19 +657,19 @@ - - -