Aplicoop desde el repo de kidekoop
This commit is contained in:
parent
69917d1ec2
commit
7cff89e418
93 changed files with 313992 additions and 0 deletions
283
website_sale_aplicoop/static/src/js/checkout_labels.js
Normal file
283
website_sale_aplicoop/static/src/js/checkout_labels.js
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/**
|
||||
* Checkout Labels Loading
|
||||
* Fetches translated labels for checkout table summary
|
||||
* IMPORTANT: This script waits for the cart to be loaded by website_sale.js
|
||||
* before rendering the checkout summary.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
console.log('[CHECKOUT] Script loaded');
|
||||
|
||||
// Get order ID from button
|
||||
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||
if (!confirmBtn) {
|
||||
console.log('[CHECKOUT] No confirm button found');
|
||||
return;
|
||||
}
|
||||
|
||||
var orderId = confirmBtn.getAttribute('data-order-id');
|
||||
if (!orderId) {
|
||||
console.log('[CHECKOUT] No order ID found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[CHECKOUT] Order ID:', orderId);
|
||||
|
||||
// Get summary div
|
||||
var summaryDiv = document.getElementById('checkout-summary');
|
||||
if (!summaryDiv) {
|
||||
console.log('[CHECKOUT] No summary div found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to fetch labels and render checkout
|
||||
var fetchLabelsAndRender = function() {
|
||||
console.log('[CHECKOUT] Fetching labels...');
|
||||
|
||||
// Wait for window.groupOrderShop.labels to be initialized (contains hardcoded labels)
|
||||
var waitForLabels = function(callback, maxWait = 3000, checkInterval = 50) {
|
||||
var startTime = Date.now();
|
||||
var checkLabels = function() {
|
||||
if (window.groupOrderShop && window.groupOrderShop.labels && Object.keys(window.groupOrderShop.labels).length > 0) {
|
||||
console.log('[CHECKOUT] ✅ Hardcoded labels found, proceeding');
|
||||
callback();
|
||||
} else if (Date.now() - startTime < maxWait) {
|
||||
setTimeout(checkLabels, checkInterval);
|
||||
} else {
|
||||
console.log('[CHECKOUT] ⚠️ Timeout waiting for labels, proceeding anyway');
|
||||
callback();
|
||||
}
|
||||
};
|
||||
checkLabels();
|
||||
};
|
||||
|
||||
waitForLabels(function() {
|
||||
// Now fetch additional labels from server
|
||||
// Detect current language from document or navigator
|
||||
var currentLang = document.documentElement.lang ||
|
||||
document.documentElement.getAttribute('lang') ||
|
||||
navigator.language ||
|
||||
'es_ES';
|
||||
console.log('[CHECKOUT] Detected language:', currentLang);
|
||||
|
||||
fetch('/eskaera/labels', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lang: currentLang
|
||||
})
|
||||
})
|
||||
.then(function(response) {
|
||||
console.log('[CHECKOUT] Response status:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
console.log('[CHECKOUT] Response data:', data);
|
||||
var serverLabels = data.result || data;
|
||||
console.log('[CHECKOUT] Server labels count:', Object.keys(serverLabels).length);
|
||||
console.log('[CHECKOUT] Sample server labels:', {
|
||||
draft_merged_success: serverLabels.draft_merged_success,
|
||||
home_delivery: serverLabels.home_delivery
|
||||
});
|
||||
|
||||
// CRITICAL: Merge server labels with existing hardcoded labels
|
||||
// Hardcoded labels MUST take precedence over server labels
|
||||
if (window.groupOrderShop && window.groupOrderShop.labels) {
|
||||
var existingLabels = window.groupOrderShop.labels;
|
||||
console.log('[CHECKOUT] Existing hardcoded labels count:', Object.keys(existingLabels).length);
|
||||
console.log('[CHECKOUT] Sample existing labels:', {
|
||||
draft_merged_success: existingLabels.draft_merged_success,
|
||||
home_delivery: existingLabels.home_delivery
|
||||
});
|
||||
|
||||
// Start with server labels, then overwrite with hardcoded ones
|
||||
var mergedLabels = Object.assign({}, serverLabels);
|
||||
Object.assign(mergedLabels, existingLabels);
|
||||
|
||||
window.groupOrderShop.labels = mergedLabels;
|
||||
console.log('[CHECKOUT] ✅ Merged labels - final count:', Object.keys(mergedLabels).length);
|
||||
console.log('[CHECKOUT] Verification:', {
|
||||
draft_merged_success: mergedLabels.draft_merged_success,
|
||||
home_delivery: mergedLabels.home_delivery
|
||||
});
|
||||
} else {
|
||||
// If no existing labels, use server labels as fallback
|
||||
if (window.groupOrderShop) {
|
||||
window.groupOrderShop.labels = serverLabels;
|
||||
}
|
||||
console.log('[CHECKOUT] ⚠️ No existing labels, using server labels');
|
||||
}
|
||||
|
||||
window.renderCheckoutSummary(window.groupOrderShop.labels);
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('[CHECKOUT] Error:', error);
|
||||
// Fallback to translated labels
|
||||
window.renderCheckoutSummary(window.getCheckoutLabels());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Listen for cart ready event instead of polling
|
||||
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
||||
// Cart already initialized, render immediately
|
||||
console.log('[CHECKOUT] Cart already ready');
|
||||
fetchLabelsAndRender();
|
||||
} else {
|
||||
// Wait for cart initialization event
|
||||
console.log('[CHECKOUT] Waiting for cart ready event...');
|
||||
document.addEventListener('groupOrderCartReady', function() {
|
||||
console.log('[CHECKOUT] Cart ready event received');
|
||||
fetchLabelsAndRender();
|
||||
}, { once: true });
|
||||
|
||||
// Fallback timeout in case event never fires
|
||||
setTimeout(function() {
|
||||
if (window.groupOrderShop && window.groupOrderShop.orderId) {
|
||||
console.log('[CHECKOUT] Fallback timeout triggered');
|
||||
fetchLabelsAndRender();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render order summary table or empty message
|
||||
* Exposed globally so other scripts can call it
|
||||
*/
|
||||
window.renderCheckoutSummary = function(labels) {
|
||||
labels = labels || window.getCheckoutLabels();
|
||||
|
||||
var summaryDiv = document.getElementById('checkout-summary');
|
||||
if (!summaryDiv) return;
|
||||
|
||||
var cartKey = 'eskaera_' + (document.getElementById('confirm-order-btn') ? document.getElementById('confirm-order-btn').getAttribute('data-order-id') : '1') + '_cart';
|
||||
var cart = JSON.parse(localStorage.getItem(cartKey) || '{}');
|
||||
|
||||
var summaryTable = summaryDiv.querySelector('.checkout-summary-table');
|
||||
var tbody = summaryDiv.querySelector('#checkout-summary-tbody');
|
||||
var totalSection = summaryDiv.querySelector('.checkout-total-section');
|
||||
|
||||
// If no table found, create it with headers (shouldn't happen, but fallback)
|
||||
if (!summaryTable) {
|
||||
var html = '<table class="table table-hover checkout-summary-table" id="checkout-summary-table" role="grid" aria-label="Purchase summary"><thead class="table-dark"><tr>' +
|
||||
'<th scope="col" class="col-name">' + escapeHtml(labels.product) + '</th>' +
|
||||
'<th scope="col" class="col-qty text-center">' + escapeHtml(labels.quantity) + '</th>' +
|
||||
'<th scope="col" class="col-price text-right">' + escapeHtml(labels.price) + '</th>' +
|
||||
'<th scope="col" class="col-subtotal text-right">' + escapeHtml(labels.subtotal) + '</th>' +
|
||||
'</tr></thead><tbody id="checkout-summary-tbody"></tbody></table>' +
|
||||
'<div class="checkout-total-section"><div class="total-row">' +
|
||||
'<span class="total-label">' + escapeHtml(labels.total) + '</span>' +
|
||||
'<span class="total-amount" id="checkout-total-amount">€0.00</span>' +
|
||||
'</div></div>';
|
||||
summaryDiv.innerHTML = html;
|
||||
summaryTable = summaryDiv.querySelector('.checkout-summary-table');
|
||||
tbody = summaryDiv.querySelector('#checkout-summary-tbody');
|
||||
totalSection = summaryDiv.querySelector('.checkout-total-section');
|
||||
}
|
||||
|
||||
// Clear only tbody, preserve headers
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (Object.keys(cart).length === 0) {
|
||||
// Show empty message if cart is empty
|
||||
var emptyRow = document.createElement('tr');
|
||||
emptyRow.id = 'checkout-empty-row';
|
||||
emptyRow.className = 'empty-message';
|
||||
emptyRow.innerHTML = '<td colspan="4" class="text-center text-muted py-4">' +
|
||||
'<i class="fa fa-inbox fa-2x mb-2"></i>' +
|
||||
'<p>' + escapeHtml(labels.empty) + '</p>' +
|
||||
'</td>';
|
||||
tbody.appendChild(emptyRow);
|
||||
|
||||
// Hide total section
|
||||
totalSection.style.display = 'none';
|
||||
} else {
|
||||
// Hide empty row if visible
|
||||
var emptyRow = tbody.querySelector('#checkout-empty-row');
|
||||
if (emptyRow) emptyRow.remove();
|
||||
|
||||
// Get delivery product ID from page data
|
||||
var checkoutPage = document.querySelector('.eskaera-checkout-page');
|
||||
var deliveryProductId = checkoutPage ? checkoutPage.getAttribute('data-delivery-product-id') : null;
|
||||
|
||||
// Separate normal products from delivery product
|
||||
var normalProducts = [];
|
||||
var deliveryProduct = null;
|
||||
|
||||
Object.keys(cart).forEach(function(productId) {
|
||||
if (productId === deliveryProductId) {
|
||||
deliveryProduct = { id: productId, item: cart[productId] };
|
||||
} else {
|
||||
normalProducts.push({ id: productId, item: cart[productId] });
|
||||
}
|
||||
});
|
||||
|
||||
// Sort normal products numerically
|
||||
normalProducts.sort(function(a, b) {
|
||||
return parseInt(a.id) - parseInt(b.id);
|
||||
});
|
||||
|
||||
var total = 0;
|
||||
|
||||
// Render normal products first
|
||||
normalProducts.forEach(function(product) {
|
||||
var item = product.item;
|
||||
var qty = parseFloat(item.quantity || item.qty || 1);
|
||||
if (isNaN(qty)) qty = 1;
|
||||
var price = parseFloat(item.price || 0);
|
||||
if (isNaN(price)) price = 0;
|
||||
var subtotal = qty * price;
|
||||
total += subtotal;
|
||||
|
||||
var row = document.createElement('tr');
|
||||
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
|
||||
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
|
||||
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
|
||||
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// Render delivery product last if present
|
||||
if (deliveryProduct) {
|
||||
var item = deliveryProduct.item;
|
||||
var qty = parseFloat(item.quantity || item.qty || 1);
|
||||
if (isNaN(qty)) qty = 1;
|
||||
var price = parseFloat(item.price || 0);
|
||||
if (isNaN(price)) price = 0;
|
||||
var subtotal = qty * price;
|
||||
total += subtotal;
|
||||
|
||||
var row = document.createElement('tr');
|
||||
row.innerHTML = '<td>' + escapeHtml(item.name) + '</td>' +
|
||||
'<td class="text-center">' + qty.toFixed(2).replace(/\.?0+$/, '') + '</td>' +
|
||||
'<td class="text-right">€' + price.toFixed(2) + '</td>' +
|
||||
'<td class="text-right">€' + subtotal.toFixed(2) + '</td>';
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
// Update total
|
||||
var totalAmount = summaryDiv.querySelector('#checkout-total-amount');
|
||||
if (totalAmount) {
|
||||
totalAmount.textContent = '€' + total.toFixed(2);
|
||||
}
|
||||
|
||||
// Show total section
|
||||
totalSection.style.display = 'block';
|
||||
}
|
||||
|
||||
console.log('[CHECKOUT] Summary rendered');
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
})();
|
||||
11
website_sale_aplicoop/static/src/js/checkout_summary.js
Normal file
11
website_sale_aplicoop/static/src/js/checkout_summary.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** AGPL-3.0
|
||||
* NOTE: Checkout summary rendering is now handled by checkout_labels.js
|
||||
* This file is kept for backwards compatibility but is no longer needed.
|
||||
* The main renderSummary() logic is in checkout_labels.js
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
// Checkout rendering is handled by checkout_labels.js
|
||||
})();
|
||||
|
||||
|
||||
207
website_sale_aplicoop/static/src/js/home_delivery.js
Normal file
207
website_sale_aplicoop/static/src/js/home_delivery.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
/**
|
||||
* Home Delivery Checkout Handler
|
||||
* Manages home delivery checkbox and product addition/removal
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var HomeDeliveryManager = {
|
||||
deliveryProductId: null,
|
||||
deliveryProductPrice: 5.74,
|
||||
deliveryProductName: 'Home Delivery', // Default fallback
|
||||
orderId: null,
|
||||
homeDeliveryEnabled: false,
|
||||
|
||||
init: function() {
|
||||
// Get delivery product info from data attributes
|
||||
var checkoutPage = document.querySelector('.eskaera-checkout-page');
|
||||
if (checkoutPage) {
|
||||
this.deliveryProductId = checkoutPage.getAttribute('data-delivery-product-id');
|
||||
console.log('[HomeDelivery] deliveryProductId from attribute:', this.deliveryProductId, 'type:', typeof this.deliveryProductId);
|
||||
|
||||
var price = checkoutPage.getAttribute('data-delivery-product-price');
|
||||
if (price) {
|
||||
this.deliveryProductPrice = parseFloat(price);
|
||||
}
|
||||
|
||||
// Get translated product name from data attribute (auto-translated by Odoo server)
|
||||
var productName = checkoutPage.getAttribute('data-delivery-product-name');
|
||||
if (productName) {
|
||||
this.deliveryProductName = productName;
|
||||
console.log('[HomeDelivery] Using translated product name from server:', this.deliveryProductName);
|
||||
}
|
||||
|
||||
// Check if home delivery is enabled for this order
|
||||
var homeDeliveryAttr = checkoutPage.getAttribute('data-home-delivery-enabled');
|
||||
this.homeDeliveryEnabled = homeDeliveryAttr === 'true' || homeDeliveryAttr === 'True';
|
||||
console.log('[HomeDelivery] Home delivery enabled:', this.homeDeliveryEnabled);
|
||||
|
||||
// Show/hide home delivery section based on configuration
|
||||
this.toggleHomeDeliverySection();
|
||||
}
|
||||
|
||||
// Get order ID from confirm button
|
||||
var confirmBtn = document.getElementById('confirm-order-btn');
|
||||
if (confirmBtn) {
|
||||
this.orderId = confirmBtn.getAttribute('data-order-id');
|
||||
console.log('[HomeDelivery] orderId from button:', this.orderId);
|
||||
}
|
||||
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
if (!checkbox) return;
|
||||
|
||||
var self = this;
|
||||
checkbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
self.addDeliveryProduct();
|
||||
self.showDeliveryInfo();
|
||||
} else {
|
||||
self.removeDeliveryProduct();
|
||||
self.hideDeliveryInfo();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if delivery product is already in cart on page load
|
||||
this.checkDeliveryInCart();
|
||||
},
|
||||
|
||||
toggleHomeDeliverySection: function() {
|
||||
var homeDeliverySection = document.querySelector('[id*="home-delivery"], [class*="home-delivery"]');
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
var homeDeliveryContainer = document.getElementById('home-delivery-container');
|
||||
|
||||
if (this.homeDeliveryEnabled) {
|
||||
// Show home delivery option
|
||||
if (checkbox) {
|
||||
checkbox.closest('.form-check').style.display = 'block';
|
||||
}
|
||||
if (homeDeliveryContainer) {
|
||||
homeDeliveryContainer.style.display = 'block';
|
||||
}
|
||||
console.log('[HomeDelivery] Home delivery option shown');
|
||||
} else {
|
||||
// Hide home delivery option and delivery info alert
|
||||
if (checkbox) {
|
||||
checkbox.closest('.form-check').style.display = 'none';
|
||||
checkbox.checked = false;
|
||||
}
|
||||
if (homeDeliveryContainer) {
|
||||
homeDeliveryContainer.style.display = 'none';
|
||||
}
|
||||
// Also hide the delivery info alert when home delivery is disabled
|
||||
this.hideDeliveryInfo();
|
||||
this.removeDeliveryProduct();
|
||||
console.log('[HomeDelivery] Home delivery option and delivery info hidden');
|
||||
}
|
||||
},
|
||||
|
||||
checkDeliveryInCart: function() {
|
||||
if (!this.deliveryProductId) return;
|
||||
|
||||
var cart = this.getCart();
|
||||
if (cart[this.deliveryProductId]) {
|
||||
var checkbox = document.getElementById('home-delivery-checkbox');
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
this.showDeliveryInfo();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getCart: function() {
|
||||
if (!this.orderId) return {};
|
||||
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||
var cartStr = localStorage.getItem(cartKey);
|
||||
return cartStr ? JSON.parse(cartStr) : {};
|
||||
},
|
||||
|
||||
saveCart: function(cart) {
|
||||
if (!this.orderId) return;
|
||||
var cartKey = 'eskaera_' + this.orderId + '_cart';
|
||||
localStorage.setItem(cartKey, JSON.stringify(cart));
|
||||
|
||||
// Re-render checkout summary without reloading
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
// Use the global function from checkout_labels.js
|
||||
if (typeof window.renderCheckoutSummary === 'function') {
|
||||
window.renderCheckoutSummary();
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
|
||||
renderCheckoutSummary: function() {
|
||||
// Stub - now handled by global window.renderCheckoutSummary
|
||||
},
|
||||
|
||||
addDeliveryProduct: function() {
|
||||
if (!this.deliveryProductId) {
|
||||
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[HomeDelivery] Adding delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||
var cart = this.getCart();
|
||||
console.log('[HomeDelivery] Current cart before adding:', cart);
|
||||
|
||||
cart[this.deliveryProductId] = {
|
||||
id: this.deliveryProductId,
|
||||
name: this.deliveryProductName,
|
||||
price: this.deliveryProductPrice,
|
||||
qty: 1
|
||||
};
|
||||
console.log('[HomeDelivery] Cart after adding delivery:', cart);
|
||||
this.saveCart(cart);
|
||||
console.log('[HomeDelivery] Delivery product added to localStorage');
|
||||
},
|
||||
|
||||
removeDeliveryProduct: function() {
|
||||
if (!this.deliveryProductId) {
|
||||
console.warn('[HomeDelivery] Delivery product ID not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[HomeDelivery] Removing delivery product - deliveryProductId:', this.deliveryProductId, 'orderId:', this.orderId);
|
||||
var cart = this.getCart();
|
||||
console.log('[HomeDelivery] Current cart before removing:', cart);
|
||||
|
||||
if (cart[this.deliveryProductId]) {
|
||||
delete cart[this.deliveryProductId];
|
||||
console.log('[HomeDelivery] Cart after removing delivery:', cart);
|
||||
}
|
||||
this.saveCart(cart);
|
||||
console.log('[HomeDelivery] Delivery product removed from localStorage');
|
||||
},
|
||||
|
||||
showDeliveryInfo: function() {
|
||||
var alert = document.getElementById('delivery-info-alert');
|
||||
if (alert) {
|
||||
console.log('[HomeDelivery] Showing delivery info alert');
|
||||
alert.classList.remove('d-none');
|
||||
alert.style.display = 'block';
|
||||
}
|
||||
},
|
||||
|
||||
hideDeliveryInfo: function() {
|
||||
var alert = document.getElementById('delivery-info-alert');
|
||||
if (alert) {
|
||||
console.log('[HomeDelivery] Hiding delivery info alert');
|
||||
alert.classList.add('d-none');
|
||||
alert.style.display = 'none';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
HomeDeliveryManager.init();
|
||||
});
|
||||
} else {
|
||||
HomeDeliveryManager.init();
|
||||
}
|
||||
|
||||
// Export to global scope
|
||||
window.HomeDeliveryManager = HomeDeliveryManager;
|
||||
})();
|
||||
67
website_sale_aplicoop/static/src/js/i18n_helpers.js
Normal file
67
website_sale_aplicoop/static/src/js/i18n_helpers.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* DEPRECATED: Use i18n_manager.js instead
|
||||
*
|
||||
* This file is kept for backwards compatibility only.
|
||||
* All translation logic has been moved to i18n_manager.js which
|
||||
* fetches translations from the server endpoint /eskaera/i18n
|
||||
*
|
||||
* Migration guide:
|
||||
* OLD: window.getCheckoutLabels()
|
||||
* NEW: i18nManager.getAll()
|
||||
*
|
||||
* OLD: window.formatCurrency(amount)
|
||||
* NEW: i18nManager.formatCurrency(amount)
|
||||
*
|
||||
* Copyright 2025 Criptomart
|
||||
* License AGPL-3.0 or later
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Keep legacy functions as wrappers for backwards compatibility
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.getAll() or i18nManager.get(key) instead
|
||||
*/
|
||||
window.getCheckoutLabels = function(key) {
|
||||
if (window.i18nManager && window.i18nManager.initialized) {
|
||||
if (key) {
|
||||
return window.i18nManager.get(key);
|
||||
}
|
||||
return window.i18nManager.getAll();
|
||||
}
|
||||
// Fallback if i18nManager not yet initialized
|
||||
return key ? key : {};
|
||||
};
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.getAll() instead
|
||||
*/
|
||||
window.getSearchLabels = function() {
|
||||
if (window.i18nManager && window.i18nManager.initialized) {
|
||||
return {
|
||||
'searchPlaceholder': window.i18nManager.get('search_products'),
|
||||
'noResults': window.i18nManager.get('no_results')
|
||||
};
|
||||
}
|
||||
return {
|
||||
'searchPlaceholder': 'Search products...',
|
||||
'noResults': 'No products found'
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* DEPRECATED - Use i18nManager.formatCurrency(amount) instead
|
||||
*/
|
||||
window.formatCurrency = function(amount) {
|
||||
if (window.i18nManager) {
|
||||
return window.i18nManager.formatCurrency(amount);
|
||||
}
|
||||
// Fallback
|
||||
return '€' + parseFloat(amount).toFixed(2);
|
||||
};
|
||||
|
||||
console.log('[i18n_helpers] DEPRECATED - Use i18n_manager.js instead');
|
||||
|
||||
})();
|
||||
153
website_sale_aplicoop/static/src/js/i18n_manager.js
Normal file
153
website_sale_aplicoop/static/src/js/i18n_manager.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* I18N Manager - Unified Translation Management
|
||||
*
|
||||
* Single point of truth for all translations.
|
||||
* Fetches from server endpoint /eskaera/i18n once and caches.
|
||||
*
|
||||
* Usage:
|
||||
* i18nManager.init().then(function() {
|
||||
* var translated = i18nManager.get('product'); // Returns translated string
|
||||
* var allLabels = i18nManager.getAll(); // Returns all labels
|
||||
* });
|
||||
*
|
||||
* Copyright 2025 Criptomart
|
||||
* License AGPL-3.0 or later
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.i18nManager = {
|
||||
labels: null,
|
||||
initialized: false,
|
||||
initPromise: null,
|
||||
|
||||
/**
|
||||
* Initialize by fetching translations from server
|
||||
* Returns a Promise that resolves when translations are loaded
|
||||
*/
|
||||
init: function() {
|
||||
if (this.initialized) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.initPromise) {
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// Detect user's language from document or fallback to en_US
|
||||
var detectedLang = document.documentElement.lang || 'es_ES';
|
||||
console.log('[i18nManager] Detected language:', detectedLang);
|
||||
|
||||
// Fetch translations from server
|
||||
this.initPromise = fetch('/eskaera/i18n', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ lang: detectedLang })
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP error, status = ' + response.status);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
// Handle JSON-RPC response format
|
||||
// Server returns: { "jsonrpc": "2.0", "id": null, "result": { labels } }
|
||||
// Extract the actual labels from the result property
|
||||
var labels = data.result || data;
|
||||
|
||||
console.log('[i18nManager] ✓ Loaded', Object.keys(labels).length, 'translation labels');
|
||||
self.labels = labels;
|
||||
self.initialized = true;
|
||||
return labels;
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error('[i18nManager] Error loading translations:', error);
|
||||
// Fallback to empty object so app doesn't crash
|
||||
self.labels = {};
|
||||
self.initialized = true;
|
||||
return {};
|
||||
});
|
||||
|
||||
return this.initPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific translation label
|
||||
* Returns the translated string or the key if not found
|
||||
*/
|
||||
get: function(key) {
|
||||
if (!this.initialized) {
|
||||
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||
return key;
|
||||
}
|
||||
return this.labels[key] || key;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all translation labels as object
|
||||
*/
|
||||
getAll: function() {
|
||||
if (!this.initialized) {
|
||||
console.warn('[i18nManager] Not yet initialized. Call init() first.');
|
||||
return {};
|
||||
}
|
||||
return this.labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a specific label exists
|
||||
*/
|
||||
has: function(key) {
|
||||
if (!this.initialized) return false;
|
||||
return key in this.labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format currency to Euro format
|
||||
*/
|
||||
formatCurrency: function(amount) {
|
||||
try {
|
||||
return new Intl.NumberFormat(document.documentElement.lang || 'es_ES', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
} catch (e) {
|
||||
// Fallback to simple Euro format
|
||||
return '€' + parseFloat(amount).toFixed(2);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
escapeHtml: function(text) {
|
||||
if (!text) return '';
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
i18nManager.init().catch(function(err) {
|
||||
console.error('[i18nManager] Auto-init failed:', err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// DOM already loaded
|
||||
setTimeout(function() {
|
||||
i18nManager.init().catch(function(err) {
|
||||
console.error('[i18nManager] Auto-init failed:', err);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
})();
|
||||
494
website_sale_aplicoop/static/src/js/realtime_search.js
Normal file
494
website_sale_aplicoop/static/src/js/realtime_search.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
1788
website_sale_aplicoop/static/src/js/website_sale.js
Normal file
1788
website_sale_aplicoop/static/src/js/website_sale.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue