Compare commits

..

No commits in common. "b15e9bc977794e187aaad03b3f436279bb04ce82" and "5eb039ffe067ae4217d5496426f211823ac8b0dd" have entirely different histories.

6 changed files with 240 additions and 479 deletions

View file

@ -4,7 +4,7 @@
[flake8] [flake8]
max-line-length = 88 max-line-length = 88
max-complexity = 30 max-complexity = 16
# B = bugbear # B = bugbear
# B9 = bugbear opinionated (incl line length) # B9 = bugbear opinionated (incl line length)
select = C,E,F,W,B,B9 select = C,E,F,W,B,B9

View file

@ -1,15 +0,0 @@
module.exports = [
{
ignores: [
"node_modules/**",
"**/*.pyc",
"**/__pycache__/**",
"ocb/**",
"setup/**",
".git/**",
"dist/**",
"build/**",
],
rules: {},
},
];

View file

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

View file

@ -7,26 +7,6 @@
console.log("[INFINITE_SCROLL] Script loaded!"); 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 () { (function () {
"use strict"; "use strict";
@ -58,14 +38,12 @@ if (typeof document !== "undefined") {
this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20; this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20;
this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1; this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1;
// Check if there are more products to load from data attribute // Check if there are more products to load
var hasNextAttr = configEl.getAttribute("data-has-next"); var hasNextBtn = document.getElementById("load-more-btn");
this.hasMore = hasNextAttr === "true" || hasNextAttr === "True"; this.hasMore = hasNextBtn && hasNextBtn.offsetParent !== null; // offsetParent === null means hidden
if (!this.hasMore) { if (!this.hasMore) {
console.log( console.log("[INFINITE_SCROLL] No more products to load");
"[INFINITE_SCROLL] No more products to load (has_next=" + hasNextAttr + ")"
);
return; return;
} }
@ -130,44 +108,6 @@ if (typeof document !== "undefined") {
console.log("[INFINITE_SCROLL] Fallback button listener attached"); 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 () { loadNextPage: function () {
var self = this; var self = this;
this.isLoading = true; this.isLoading = true;

View file

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

View file

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