diff --git a/.flake8 b/.flake8 index d5708c3..cb28e98 100644 --- a/.flake8 +++ b/.flake8 @@ -4,7 +4,7 @@ [flake8] max-line-length = 88 -max-complexity = 30 +max-complexity = 16 # B = bugbear # B9 = bugbear opinionated (incl line length) select = C,E,F,W,B,B9 diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 004e3e6..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = [ - { - ignores: [ - "node_modules/**", - "**/*.pyc", - "**/__pycache__/**", - "ocb/**", - "setup/**", - ".git/**", - "dist/**", - "build/**", - ], - rules: {}, - }, -]; diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 2ca0776..14b3f91 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="http", + type="json", auth="user", website=True, methods=["POST"], @@ -1407,27 +1407,17 @@ class AplicoopWebsiteSale(WebsiteSale): ) ) - # Parse JSON body for parameters (type="http" doesn't auto-parse JSON) - params = {} + # Get page from POST try: - 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))) + page = int(post.get("page", 1)) if page < 1: page = 1 except (ValueError, TypeError): page = 1 # Get filters - search_query = params.get("search", post.get("search", "")).strip() - category_filter = str(params.get("category", post.get("category", "0"))) + search_query = post.get("search", "").strip() + category_filter = str(post.get("category", "0")) _logger.info( "load_products_ajax: order_id=%d, page=%d, search=%s, category=%s", @@ -1487,8 +1477,7 @@ class AplicoopWebsiteSale(WebsiteSale): lambda p: p.categ_id.id in order_cat_ids ) - # Preserve search filter by using intersection - filtered_products = filtered_products & cat_filtered + filtered_products = cat_filtered except (ValueError, TypeError) as e: _logger.warning( "load_products_ajax: Invalid category filter: %s", str(e) @@ -1567,7 +1556,7 @@ class AplicoopWebsiteSale(WebsiteSale): } # Render HTML - html = request.env["ir.ui.view"]._render_template( + html = request.env["ir.ui.view"]._render( "website_sale_aplicoop.eskaera_shop_products", { "group_order": group_order, @@ -1582,18 +1571,13 @@ class AplicoopWebsiteSale(WebsiteSale): }, ) - 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")], - ) + return { + "html": html, + "has_next": has_next, + "next_page": page + 1, + "total": total_products, + "page": page, + } @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 65b6f35..99b7723 100644 --- a/website_sale_aplicoop/static/src/js/infinite_scroll.js +++ b/website_sale_aplicoop/static/src/js/infinite_scroll.js @@ -7,26 +7,6 @@ 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"; @@ -58,14 +38,12 @@ if (typeof document !== "undefined") { 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 from data attribute - var hasNextAttr = configEl.getAttribute("data-has-next"); - this.hasMore = hasNextAttr === "true" || hasNextAttr === "True"; + // 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 if (!this.hasMore) { - console.log( - "[INFINITE_SCROLL] No more products to load (has_next=" + hasNextAttr + ")" - ); + console.log("[INFINITE_SCROLL] No more products to load"); return; } @@ -130,44 +108,6 @@ if (typeof document !== "undefined") { 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 71fd4cb..a8da9c8 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,59 +16,57 @@ 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]; @@ -79,7 +77,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) { @@ -88,407 +86,286 @@ 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; - - // 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(); - } + 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 @@ -498,57 +375,47 @@ } 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++; } }); @@ -557,85 +424,71 @@ 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 1d135ba..ce07ce2 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -586,7 +586,6 @@ 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"> @@ -657,19 +656,19 @@ - - -