Aplicoop desde el repo de kidekoop

This commit is contained in:
snt 2026-02-11 15:32:11 +01:00
parent 69917d1ec2
commit 7cff89e418
93 changed files with 313992 additions and 0 deletions

View file

@ -0,0 +1,494 @@
/*
* Copyright 2025 Criptomart
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
*/
(function() {
'use strict';
window.realtimeSearch = {
searchInput: null,
categorySelect: null,
allProducts: [],
debounceTimer: null,
debounceDelay: 0,
categoryHierarchy: {}, // Maps parent category IDs to their children
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...');
// searchInput y categorySelect ya fueron asignados por tryInit()
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!');
return false;
}
if (!this.categorySelect) {
console.error('[realtimeSearch] ERROR: Category select not found!');
return false;
}
this._buildCategoryHierarchyFromDOM();
this._storeAllProducts();
console.log('[realtimeSearch] After _storeAllProducts(), calling _attachEventListeners()...');
this._attachEventListeners();
console.log('[realtimeSearch] ✓ Initialized successfully');
return true;
},
_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 optionStack = []; // Stack para mantener los padres en cada nivel
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];
if (!self.categoryHierarchy[parentId]) {
self.categoryHierarchy[parentId] = [];
}
if (!self.categoryHierarchy[parentId].includes(categoryId)) {
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) {
optionStack[depth] = categoryId;
} else {
optionStack.push(categoryId);
}
});
console.log('[realtimeSearch] Complete category hierarchy built:', self.categoryHierarchy);
},
_storeAllProducts: function() {
var productCards = document.querySelectorAll('.product-card');
console.log('[realtimeSearch] Found ' + productCards.length + ' product cards');
var 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') || '';
// 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);
});
}
self.allProducts.push({
element: card,
name: name.toLowerCase(),
category: categoryId.toString(),
originalCategory: categoryId,
tags: tagIds // Array of tag IDs for this product
});
});
console.log('[realtimeSearch] Total products stored: ' + this.allProducts.length);
},
_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 = '';
// Prevent form submission completely
var form = self.searchInput.closest('form');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
e.stopPropagation();
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') {
e.preventDefault();
e.stopPropagation();
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) {
try {
console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"');
self._filterProducts();
} catch (error) {
console.error('[realtimeSearch] Error in input listener:', error.message);
}
});
// Also keep 'keyup' for extra compatibility
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);
}
});
// Category select
self.categorySelect.addEventListener('change', function(e) {
try {
console.log('[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"');
self._filterProducts();
} catch (error) {
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');
// 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);
// 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');
// 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);
} else {
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) {
e.preventDefault();
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
var originalColor = self.originalTagColors[tagId];
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');
} else {
// Select
self.selectedTags.add(tagId);
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);
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)');
} 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)');
} 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)');
}
});
// 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);
var pollingCounter = 0;
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 || '') : '';
// 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('═══════════════════════════════════════════');
}
// Log every 20 polls (reduce spam)
if (pollingCounter % 20 === 0) {
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 + '")');
self.lastSearchValue = currentSearchValue;
self.lastCategoryValue = currentCategoryValue;
self._filterProducts();
}
} catch (error) {
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');
},
_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');
var count = countSpan ? parseInt(countSpan.textContent, 10) : 0;
self.availableTags[tagId] = {
id: tagId,
name: tagName,
count: count
};
});
console.log('[realtimeSearch] Initialized ' + Object.keys(self.availableTags).length + ' available tags');
},
_filterProducts: function() {
var self = this;
try {
var searchQuery = (self.searchInput.value || '').toLowerCase().trim();
var selectedCategoryId = (self.categorySelect.value || '').toString();
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 descendants = [];
if (self.categoryHierarchy[parentId]) {
self.categoryHierarchy[parentId].forEach(function(childId) {
descendants.push(childId);
allowedCategories[childId] = true;
// Recursivamente obtener descendientes del hijo
var grandDescendants = getAllDescendants(childId);
descendants = descendants.concat(grandDescendants);
});
}
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));
}
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) {
var nameMatches = !searchQuery || product.name.indexOf(searchQuery) !== -1;
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) {
return self.selectedTags.has(productTagId);
});
}
var shouldShow = nameMatches && categoryMatches && tagMatches;
if (shouldShow) {
product.element.classList.remove('hidden-product');
visibleCount++;
// Count this product's tags toward the dynamic counters
product.tags.forEach(function(tagId) {
if (tagCounts.hasOwnProperty(tagId)) {
tagCounts[tagId]++;
}
});
} else {
product.element.classList.add('hidden-product');
hiddenCount++;
}
});
// Update badge counts dynamically
for (var tagId in tagCounts) {
var badge = document.querySelector('[data-tag-id="' + tagId + '"]');
if (badge) {
var countSpan = badge.querySelector('.tag-count');
if (countSpan) {
countSpan.textContent = tagCounts[tagId];
}
}
}
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);
}
}
};
// Initialize when DOM is ready
console.log('[realtimeSearch] Script loaded, DOM state: ' + document.readyState);
function tryInit() {
try {
console.log('[realtimeSearch] Attempting initialization...');
// Query 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);
if (searchInput) {
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);
if (productCards.length > 0 && searchInput) {
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!');
} else {
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);
}
setTimeout(tryInit, 500);
}
} catch (error) {
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');
tryInit();
});
} else {
console.log('[realtimeSearch] DOM already loaded, initializing with delay');
setTimeout(tryInit, 500);
}
})();