diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index 740e206..64d5662 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -48,7 +48,17 @@ "assets": { "web.assets_frontend": [ "website_sale_aplicoop/static/src/css/website_sale.css", + # i18n and helpers must load first + "website_sale_aplicoop/static/src/js/i18n_manager.js", + "website_sale_aplicoop/static/src/js/i18n_helpers.js", + # Core shop functionality + "website_sale_aplicoop/static/src/js/website_sale.js", + "website_sale_aplicoop/static/src/js/checkout_labels.js", + "website_sale_aplicoop/static/src/js/home_delivery.js", + "website_sale_aplicoop/static/src/js/checkout_summary.js", + # Search and pagination "website_sale_aplicoop/static/src/js/infinite_scroll.js", + "website_sale_aplicoop/static/src/js/realtime_search.js", ], "web.assets_tests": [ "website_sale_aplicoop/static/tests/test_suite.js", diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 2ca0776..da0971a 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -1441,12 +1441,17 @@ class AplicoopWebsiteSale(WebsiteSale): all_products = group_order._get_products_for_group_order(group_order.id) filtered_products = all_products - # Apply search + # Apply search filter (only if search_query is not empty) if search_query: + _logger.info("load_products_ajax: Applying search filter: %s", search_query) filtered_products = filtered_products.filtered( lambda p: search_query.lower() in p.name.lower() or search_query.lower() in (p.description or "").lower() ) + _logger.info( + "load_products_ajax: After search filter: %d products", + len(filtered_products), + ) # Apply category filter if category_filter != "0": @@ -1455,6 +1460,11 @@ class AplicoopWebsiteSale(WebsiteSale): selected_category = request.env["product.category"].browse(category_id) if selected_category.exists(): + _logger.info( + "load_products_ajax: Applying category filter: %d (%s)", + category_id, + selected_category.name, + ) all_category_ids = [category_id] def get_all_children(category): @@ -1489,6 +1499,10 @@ class AplicoopWebsiteSale(WebsiteSale): # Preserve search filter by using intersection filtered_products = filtered_products & cat_filtered + _logger.info( + "load_products_ajax: After category filter: %d products", + len(filtered_products), + ) except (ValueError, TypeError) as e: _logger.warning( "load_products_ajax: Invalid category filter: %s", str(e) @@ -1500,6 +1514,16 @@ class AplicoopWebsiteSale(WebsiteSale): products_page = filtered_products[offset : offset + per_page] has_next = offset + per_page < total_products + _logger.info( + "load_products_ajax: Pagination - page=%d, offset=%d, per_page=%d, " + "total=%d, has_next=%s", + page, + offset, + per_page, + total_products, + has_next, + ) + # Get prices pricelist = self._resolve_pricelist() product_price_info = {} diff --git a/website_sale_aplicoop/static/src/js/infinite_scroll.js b/website_sale_aplicoop/static/src/js/infinite_scroll.js index 65b6f35..1ccf661 100644 --- a/website_sale_aplicoop/static/src/js/infinite_scroll.js +++ b/website_sale_aplicoop/static/src/js/infinite_scroll.js @@ -7,25 +7,105 @@ 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); +// DEBUG: Add MutationObserver to detect WHO is clearing the products grid +(function () { + var setupGridObserver = function () { + var grid = document.getElementById("products-grid"); + if (!grid) { + console.log("[MUTATION_DEBUG] products-grid not found yet, will retry..."); + setTimeout(setupGridObserver, 100); + return; + } + + console.log("[MUTATION_DEBUG] 🔍 Setting up MutationObserver on products-grid"); + console.log("[MUTATION_DEBUG] Initial child count:", grid.children.length); + console.log("[MUTATION_DEBUG] Grid innerHTML length:", grid.innerHTML.length); + + // Watch the grid itself for child changes + var gridObserver = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.type === "childList") { + if (mutation.removedNodes.length > 0) { + console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS REMOVED FROM GRID!"); + console.log( + "[MUTATION_DEBUG] Removed nodes count:", + mutation.removedNodes.length + ); + console.log("[MUTATION_DEBUG] Stack trace:"); + console.trace(); + } + if (mutation.addedNodes.length > 0) { + console.log("[MUTATION_DEBUG] Products added:", mutation.addedNodes.length); + } + } + }); + }); + gridObserver.observe(grid, { childList: true, subtree: false }); + + // ALSO watch the parent for the grid element itself being replaced/removed + var parent = grid.parentElement; + if (parent) { + console.log( + "[MUTATION_DEBUG] 🔍 Also watching parent element:", + parent.tagName, + parent.className + ); + var parentObserver = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + if (mutation.type === "childList") { + mutation.removedNodes.forEach(function (node) { + if ( + node.id === "products-grid" || + (node.querySelector && node.querySelector("#products-grid")) + ) { + console.log( + "[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS-GRID ELEMENT ITSELF WAS REMOVED!" + ); + console.log("[MUTATION_DEBUG] Stack trace:"); + console.trace(); + } + }); + } + }); + }); + parentObserver.observe(parent, { childList: true, subtree: true }); + } + + // Poll to detect innerHTML being cleared (as backup) + var lastChildCount = grid.children.length; + setInterval(function () { + var currentGrid = document.getElementById("products-grid"); + if (!currentGrid) { + console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID ELEMENT NO LONGER EXISTS!"); + console.trace(); + return; + } + var currentChildCount = currentGrid.children.length; + if (currentChildCount !== lastChildCount) { + console.log( + "[MUTATION_DEBUG] 📊 Child count changed: " + + lastChildCount + + " → " + + currentChildCount + ); + if (currentChildCount === 0 && lastChildCount > 0) { + console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID WAS EMPTIED!"); + console.trace(); + } + lastChildCount = currentChildCount; + } + }, 100); + + console.log("[MUTATION_DEBUG] ✅ Observers attached (grid + parent + polling)"); + }; + + // Start observing as soon as possible + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setupGridObserver); + } else { + setupGridObserver(); } -} +})(); (function () { "use strict"; @@ -45,10 +125,16 @@ if (typeof document !== "undefined") { config: {}, init: function () { + console.log("[INFINITE_SCROLL] 🔧 init() called"); + // Get configuration from page data var configEl = document.getElementById("eskaera-config"); + console.log("[INFINITE_SCROLL] eskaera-config element:", configEl); + if (!configEl) { - console.log("[INFINITE_SCROLL] No eskaera-config found, lazy loading disabled"); + console.error( + "[INFINITE_SCROLL] ❌ No eskaera-config found, lazy loading disabled" + ); return; } @@ -58,15 +144,33 @@ if (typeof document !== "undefined") { this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20; this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1; + console.log("[INFINITE_SCROLL] Config loaded:", { + orderId: this.orderId, + searchQuery: this.searchQuery, + category: this.category, + perPage: this.perPage, + currentPage: this.currentPage, + }); + // Check if there are more products to load from data attribute var hasNextAttr = configEl.getAttribute("data-has-next"); this.hasMore = hasNextAttr === "true" || hasNextAttr === "True"; + console.log( + "[INFINITE_SCROLL] hasMore=" + + this.hasMore + + " (data-has-next=" + + hasNextAttr + + ")" + ); + if (!this.hasMore) { console.log( - "[INFINITE_SCROLL] No more products to load (has_next=" + hasNextAttr + ")" + "[INFINITE_SCROLL] ⚠️ No more pages available, but keeping initialized for filter handling (has_next=" + + hasNextAttr + + ")" ); - return; + // Don't return - we need to stay initialized so realtime_search can call resetWithFilters() } console.log("[INFINITE_SCROLL] Initialized with:", { @@ -77,36 +181,50 @@ if (typeof document !== "undefined") { currentPage: this.currentPage, }); - this.attachScrollListener(); - // Also keep the button listener as fallback - this.attachFallbackButtonListener(); + // Only attach scroll listener if there are more pages to load + if (this.hasMore) { + this.attachScrollListener(); + this.attachFallbackButtonListener(); + } else { + console.log("[INFINITE_SCROLL] Skipping scroll listener (no more pages)"); + } }, attachScrollListener: function () { var self = this; - var scrollThreshold = 0.8; // Load when 80% scrolled + var scrollThreshold = 300; // Load when within 300px of the bottom of the grid window.addEventListener("scroll", function () { if (self.isLoading || !self.hasMore) { return; } - var scrollHeight = document.documentElement.scrollHeight; - var scrollTop = window.scrollY; - var clientHeight = window.innerHeight; - var scrollPercent = (scrollTop + clientHeight) / scrollHeight; + var grid = document.getElementById("products-grid"); + if (!grid) { + return; + } - if (scrollPercent >= scrollThreshold) { + // Calculate distance from bottom of grid to bottom of viewport + var gridRect = grid.getBoundingClientRect(); + var gridBottom = gridRect.bottom; + var viewportBottom = window.innerHeight; + var distanceFromBottom = gridBottom - viewportBottom; + + // Load more if we're within threshold pixels of the grid bottom + if (distanceFromBottom <= scrollThreshold && distanceFromBottom > 0) { console.log( - "[INFINITE_SCROLL] Scroll threshold reached, loading next page" + "[INFINITE_SCROLL] Near grid bottom (distance: " + + Math.round(distanceFromBottom) + + "px), loading next page" ); self.loadNextPage(); } }); console.log( - "[INFINITE_SCROLL] Scroll listener attached (threshold:", - scrollThreshold * 100 + "%)" + "[INFINITE_SCROLL] Scroll listener attached (threshold: " + + scrollThreshold + + "px from grid bottom)" ); }, @@ -134,20 +252,53 @@ if (typeof document !== "undefined") { /** * Reset infinite scroll to page 1 with new filters and reload products. * Called by realtime_search when filters change. + * + * WARNING: This clears the grid! Only call when filters actually change. */ console.log( - "[INFINITE_SCROLL] Resetting with filters: search=" + + "[INFINITE_SCROLL] ⚠️⚠️⚠️ resetWithFilters CALLED - search=" + searchQuery + " category=" + categoryId ); + console.trace("[INFINITE_SCROLL] ⚠️⚠️⚠️ WHO CALLED resetWithFilters? Call stack:"); - this.searchQuery = searchQuery || ""; - this.category = categoryId || "0"; + // Normalize values: empty string to "", null to "0" for category + var newSearchQuery = (searchQuery || "").trim(); + var newCategory = (categoryId || "").trim() || "0"; + + // CHECK IF VALUES ACTUALLY CHANGED before clearing grid! + if (newSearchQuery === this.searchQuery && newCategory === this.category) { + console.log( + "[INFINITE_SCROLL] ✅ NO CHANGE - Skipping reset (values are identical)" + ); + return; // Don't clear grid if nothing changed! + } + + console.log( + "[INFINITE_SCROLL] 🔥 VALUES CHANGED - Old: search=" + + this.searchQuery + + " category=" + + this.category + + " → New: search=" + + newSearchQuery + + " category=" + + newCategory + ); + + this.searchQuery = newSearchQuery; + this.category = newCategory; this.currentPage = 0; // Set to 0 so loadNextPage() increments to 1 this.isLoading = false; this.hasMore = true; + console.log( + "[INFINITE_SCROLL] After normalization: search=" + + this.searchQuery + + " category=" + + this.category + ); + // Update the config element data attributes for consistency var configEl = document.getElementById("eskaera-config"); if (configEl) { @@ -155,26 +306,58 @@ if (typeof document !== "undefined") { configEl.setAttribute("data-category", this.category); configEl.setAttribute("data-current-page", "1"); configEl.setAttribute("data-has-next", "true"); + console.log("[INFINITE_SCROLL] Updated eskaera-config attributes"); } // Clear the grid and reload from page 1 var grid = document.getElementById("products-grid"); if (grid) { + console.log("[INFINITE_SCROLL] 🗑️ CLEARING GRID NOW!"); grid.innerHTML = ""; console.log("[INFINITE_SCROLL] Grid cleared"); } // Load first page with new filters + console.log("[INFINITE_SCROLL] Calling loadNextPage()..."); this.loadNextPage(); }, loadNextPage: function () { + console.log( + "[INFINITE_SCROLL] 🚀 loadNextPage() CALLED - currentPage=" + + this.currentPage + + " isLoading=" + + this.isLoading + + " hasMore=" + + this.hasMore + ); + + if (this.isLoading || !this.hasMore) { + console.log("[INFINITE_SCROLL] ❌ ABORTING - already loading or no more pages"); + return; + } + var self = this; this.isLoading = true; - this.currentPage += 1; + + // Only increment if we're not loading first page (currentPage will be 0 after reset) + if (this.currentPage === 0) { + console.log( + "[INFINITE_SCROLL] ✅ Incrementing from 0 to 1 (first page after reset)" + ); + this.currentPage = 1; + } else { + console.log( + "[INFINITE_SCROLL] ✅ Incrementing page " + + this.currentPage + + " → " + + (this.currentPage + 1) + ); + this.currentPage += 1; + } console.log( - "[INFINITE_SCROLL] Loading page", + "[INFINITE_SCROLL] 📡 About to fetch page", this.currentPage, "for order", this.orderId diff --git a/website_sale_aplicoop/static/src/js/realtime_search.js b/website_sale_aplicoop/static/src/js/realtime_search.js index 71fd4cb..0cdc792 100644 --- a/website_sale_aplicoop/static/src/js/realtime_search.js +++ b/website_sale_aplicoop/static/src/js/realtime_search.js @@ -135,6 +135,9 @@ _attachEventListeners: function () { var self = this; + // Flag to prevent filtering during initialization + self.isInitializing = true; + // Initialize available tags from DOM self._initializeAvailableTags(); @@ -142,8 +145,48 @@ self.originalTagColors = {}; // Maps tag ID to original color // Store last values at instance level so polling can access them - self.lastSearchValue = ""; - self.lastCategoryValue = ""; + // Initialize to current values to avoid triggering reset on first poll + self.lastSearchValue = self.searchInput.value.trim(); + self.lastCategoryValue = self.categorySelect.value; + + console.log( + "[realtimeSearch] Initial values stored - search:", + JSON.stringify(self.lastSearchValue), + "category:", + JSON.stringify(self.lastCategoryValue) + ); + + // Clear search button - DISABLED FOR NOW + // TODO: Re-implement without causing initialization issues + /* + self.clearSearchBtn = document.getElementById("clear-search-btn"); + if (self.clearSearchBtn) { + // Show/hide button based on input content (passive, no filtering) + self.searchInput.addEventListener("input", function () { + if (self.searchInput.value.trim().length > 0) { + self.clearSearchBtn.style.display = "block"; + } else { + self.clearSearchBtn.style.display = "none"; + } + }); + + // Clear search when button clicked + self.clearSearchBtn.addEventListener("click", function (e) { + e.preventDefault(); + console.log("[realtimeSearch] Clear search button clicked"); + self.searchInput.value = ""; + self.clearSearchBtn.style.display = "none"; + self.searchInput.focus(); + // Trigger input event to update search results + self.searchInput.dispatchEvent(new Event("input", { bubbles: true })); + }); + + // Initial check + if (self.searchInput.value.trim().length > 0) { + self.clearSearchBtn.style.display = "block"; + } + } + */ // Prevent form submission completely var form = self.searchInput.closest("form"); @@ -169,6 +212,11 @@ // Search input: listen to 'input' for real-time filtering self.searchInput.addEventListener("input", function (e) { try { + // Skip filtering during initialization + if (self.isInitializing) { + console.log("[realtimeSearch] INPUT event during init - skipping filter"); + return; + } console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"'); self._filterProducts(); } catch (error) { @@ -179,6 +227,11 @@ // Also keep 'keyup' for extra compatibility self.searchInput.addEventListener("keyup", function (e) { try { + // Skip filtering during initialization + if (self.isInitializing) { + console.log("[realtimeSearch] KEYUP event during init - skipping filter"); + return; + } console.log('[realtimeSearch] KEYUP event - value: "' + e.target.value + '"'); self._filterProducts(); } catch (error) { @@ -189,6 +242,11 @@ // Category select self.categorySelect.addEventListener("change", function (e) { try { + // Skip filtering during initialization + if (self.isInitializing) { + console.log("[realtimeSearch] CHANGE event during init - skipping filter"); + return; + } console.log( '[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"' ); @@ -315,7 +373,10 @@ }); // Filter products (independent of search/category state) - self._filterProducts(); + // Skip during initialization + if (!self.isInitializing) { + self._filterProducts(); + } }); }); @@ -328,6 +389,11 @@ var pollingCounter = 0; var pollInterval = setInterval(function () { try { + // Skip polling during initialization to avoid clearing products + if (self.isInitializing) { + return; + } + pollingCounter++; // Try multiple ways to get the search value @@ -418,10 +484,13 @@ ); } else { // Fallback: filter locally (but this only filters loaded products) - console.log( - "[realtimeSearch] infiniteScroll not available, filtering locally only" - ); - self._filterProducts(); + // Skip during initialization + if (!self.isInitializing) { + console.log( + "[realtimeSearch] infiniteScroll not available, filtering locally only" + ); + self._filterProducts(); + } } } } catch (error) { @@ -432,6 +501,10 @@ console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval); console.log("[realtimeSearch] Event listeners attached with polling fallback"); + + // Initialization complete - allow filtering now + self.isInitializing = false; + console.log("[realtimeSearch] ✅ Initialization complete - filtering enabled"); }, _initializeAvailableTags: function () { diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 1a17339..4123839 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -45,6 +45,10 @@ console.log("Initializing cart for order:", this.orderId); + // Attach event listeners FIRST (doesn't depend on translations) + this._attachEventListeners(); + console.log("[groupOrderShop] Event listeners attached"); + // Wait for i18nManager to load translations from server i18nManager .init() @@ -52,8 +56,6 @@ console.log("[groupOrderShop] Translations loaded from server"); self.labels = i18nManager.getAll(); - // Initialize event listeners and state after translations are ready - self._attachEventListeners(); self._loadCart(); self._checkConfirmationMessage(); self._initializeTooltips(); @@ -603,12 +605,70 @@ _attachEventListeners: function () { var self = this; + // Helper function to round decimals correctly + function roundDecimal(value, decimals) { + var factor = Math.pow(10, decimals); + return Math.round(value * factor) / factor; + } + + // ============ ATTACH CHECKOUT BUTTONS (ALWAYS, on any page) ============ + // These buttons exist on checkout page, not on cart pages + if (!this._cartCheckoutListenersAttached) { + console.log("[_attachEventListeners] Attaching checkout button listeners..."); + + // Button to save as draft (in checkout page) + var saveBtn = document.getElementById("save-order-btn"); + console.log("[_attachEventListeners] save-order-btn found:", !!saveBtn); + + if (saveBtn) { + saveBtn.addEventListener("click", function (e) { + console.log("[CLICK] save-order-btn clicked"); + e.preventDefault(); + self._saveOrderDraft(); + }); + } + + // Confirm order button (in checkout page) + var confirmBtn = document.getElementById("confirm-order-btn"); + console.log("[_attachEventListeners] confirm-order-btn found:", !!confirmBtn); + + if (confirmBtn) { + confirmBtn.addEventListener("click", function (e) { + console.log("[CLICK] confirm-order-btn clicked"); + e.preventDefault(); + self._confirmOrder(); + }); + } + + // Button to reload from draft (in My Cart header - cart pages) + var reloadCartBtn = document.getElementById("reload-cart-btn"); + console.log("[_attachEventListeners] reload-cart-btn found:", !!reloadCartBtn); + + if (reloadCartBtn) { + reloadCartBtn.addEventListener("click", function (e) { + console.log("[CLICK] reload-cart-btn clicked"); + e.preventDefault(); + self._loadDraftCart(); + }); + } + + this._cartCheckoutListenersAttached = true; + console.log("[_attachEventListeners] Checkout listeners attached (one-time)"); + } + // ============ LAZY LOADING: Load More Button ============ this._attachLoadMoreListener(); - // Adjust quantity step based on UoM category - // Categories without decimals (per unit): "Unit", "Units", etc. - // Categories with decimals: "Weight", "Volume", "Length", etc. + // ============ USE EVENT DELEGATION FOR QUANTITY & CART BUTTONS ============ + // This way, new products loaded via AJAX will automatically have listeners + var productsGrid = document.getElementById("products-grid"); + + if (!productsGrid) { + console.log("[_attachEventListeners] No products-grid found (checkout page?)"); + return; + } + + // First, adjust quantity steps for all existing inputs var unitInputs = document.querySelectorAll(".product-qty"); console.log("=== ADJUSTING QUANTITY STEPS ==="); console.log("Found " + unitInputs.length + " quantity inputs"); @@ -638,141 +698,108 @@ } console.log("=== END ADJUSTING QUANTITY STEPS ==="); - // Botones + y - para aumentar/disminuir cantidad - // Helper function to round decimals correctly - function roundDecimal(value, decimals) { - var factor = Math.pow(10, decimals); - return Math.round(value * factor) / factor; + // IMPORTANT: Do NOT clone the grid node - this destroys all products! + // Instead, use a flag to prevent adding duplicate event listeners + if (productsGrid._delegationListenersAttached) { + console.log( + "[_attachEventListeners] Grid delegation listeners already attached, skipping" + ); + return; } + productsGrid._delegationListenersAttached = true; + console.log("[_attachEventListeners] Attaching grid delegation listeners (one-time)"); - // Remove old listeners by cloning elements (to avoid duplication) - var decreaseButtons = document.querySelectorAll(".qty-decrease"); - for (var k = 0; k < decreaseButtons.length; k++) { - var newBtn = decreaseButtons[k].cloneNode(true); - decreaseButtons[k].parentNode.replaceChild(newBtn, decreaseButtons[k]); - } + // Quantity decrease button (via event delegation) + productsGrid.addEventListener("click", function (e) { + var decreaseBtn = e.target.closest(".qty-decrease"); + if (!decreaseBtn) return; - var increaseButtons = document.querySelectorAll(".qty-increase"); - for (var k = 0; k < increaseButtons.length; k++) { - var newBtn = increaseButtons[k].cloneNode(true); - increaseButtons[k].parentNode.replaceChild(newBtn, increaseButtons[k]); - } + e.preventDefault(); + var productId = decreaseBtn.getAttribute("data-product-id"); + var input = document.getElementById("qty_" + productId); + if (!input) return; - // Ahora asignar nuevos listeners - decreaseButtons = document.querySelectorAll(".qty-decrease"); - for (var k = 0; k < decreaseButtons.length; k++) { - decreaseButtons[k].addEventListener("click", function (e) { - e.preventDefault(); - var productId = this.getAttribute("data-product-id"); - var input = document.getElementById("qty_" + productId); - if (!input) return; + var step = parseFloat(input.step) || 1; + var currentValue = parseFloat(input.value) || 0; + var min = parseFloat(input.min) || 0; + var newValue = Math.max(min, roundDecimal(currentValue - step, 1)); - var step = parseFloat(input.step) || 1; - var currentValue = parseFloat(input.value) || 0; - var min = parseFloat(input.min) || 0; - var newValue = Math.max(min, roundDecimal(currentValue - step, 1)); + // Si es unidad, mostrar como entero + if (input.dataset.isUnit === "true") { + input.value = Math.floor(newValue); + } else { + input.value = newValue; + } + }); - // Si es unidad, mostrar como entero - if (input.dataset.isUnit === "true") { - input.value = Math.floor(newValue); - } else { - input.value = newValue; - } + // Quantity increase button (via event delegation) + productsGrid.addEventListener("click", function (e) { + var increaseBtn = e.target.closest(".qty-increase"); + if (!increaseBtn) return; + + e.preventDefault(); + var productId = increaseBtn.getAttribute("data-product-id"); + var input = document.getElementById("qty_" + productId); + if (!input) return; + + var step = parseFloat(input.step) || 1; + var currentValue = parseFloat(input.value) || 0; + var newValue = roundDecimal(currentValue + step, 1); + + // Si es unidad, mostrar como entero + if (input.dataset.isUnit === "true") { + input.value = Math.floor(newValue); + } else { + input.value = newValue; + } + }); + + // Add to cart button (via event delegation) + productsGrid.addEventListener("click", function (e) { + var cartBtn = e.target.closest(".add-to-cart-btn"); + if (!cartBtn) return; + + e.preventDefault(); + var form = cartBtn.closest(".add-to-cart-form"); + var productId = form.getAttribute("data-product-id"); + var productName = form.getAttribute("data-product-name") || "Product"; + var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0; + var quantityInput = form.querySelector(".product-qty"); + var quantity = quantityInput ? parseFloat(quantityInput.value) : 1; + + console.log("Adding:", { + productId: productId, + productName: productName, + productPrice: productPrice, + quantity: quantity, }); - } - increaseButtons = document.querySelectorAll(".qty-increase"); - for (var k = 0; k < increaseButtons.length; k++) { - increaseButtons[k].addEventListener("click", function (e) { - e.preventDefault(); - var productId = this.getAttribute("data-product-id"); - var input = document.getElementById("qty_" + productId); - if (!input) return; - - var step = parseFloat(input.step) || 1; - var currentValue = parseFloat(input.value) || 0; - var newValue = roundDecimal(currentValue + step, 1); - - // Si es unidad, mostrar como entero - if (input.dataset.isUnit === "true") { - input.value = Math.floor(newValue); - } else { - input.value = newValue; - } - }); - } - - // Botones de agregar al carrito - var buttons = document.querySelectorAll(".add-to-cart-btn"); - for (var i = 0; i < buttons.length; i++) { - buttons[i].addEventListener("click", function (e) { - e.preventDefault(); - var form = this.closest(".add-to-cart-form"); - var productId = form.getAttribute("data-product-id"); - var productName = form.getAttribute("data-product-name") || "Product"; - var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0; - var quantityInput = form.querySelector(".product-qty"); - var quantity = quantityInput ? parseFloat(quantityInput.value) : 1; - - console.log("Adding:", { - productId: productId, - productName: productName, - productPrice: productPrice, - quantity: quantity, - }); - - if (quantity > 0) { - self._addToCart(productId, productName, productPrice, quantity); - } else { - var labels = self._getLabels(); - self._showNotification( - labels.invalid_quantity || "Please enter a valid quantity", - "warning" - ); - } - }); - } + if (quantity > 0) { + self._addToCart(productId, productName, productPrice, quantity); + } else { + var labels = self._getLabels(); + self._showNotification( + labels.invalid_quantity || "Please enter a valid quantity", + "warning" + ); + } + }); // Button to save cart as draft (in My Cart header) - var savCartBtn = document.getElementById("save-cart-btn"); - if (savCartBtn) { - // Remove old listeners by cloning - var savCartBtnNew = savCartBtn.cloneNode(true); - savCartBtn.parentNode.replaceChild(savCartBtnNew, savCartBtn); - savCartBtnNew.addEventListener("click", function (e) { - e.preventDefault(); - self._saveCartAsDraft(); - }); - } + // Only attach ONCE + if (!this._cartCheckoutListenersAttached) { + console.log("[_attachEventListeners] Attempting to attach checkout listeners..."); - // Button to reload from draft (in My Cart header) - var reloadCartBtn = document.getElementById("reload-cart-btn"); - if (reloadCartBtn) { - // Remove old listeners by cloning - var reloadCartBtnNew = reloadCartBtn.cloneNode(true); - reloadCartBtn.parentNode.replaceChild(reloadCartBtnNew, reloadCartBtn); - reloadCartBtnNew.addEventListener("click", function (e) { - e.preventDefault(); - self._loadDraftCart(); - }); - } + var savCartBtn = document.getElementById("save-cart-btn"); + console.log("[_attachEventListeners] save-cart-btn found:", !!savCartBtn); - // Button to save as draft - var saveBtn = document.getElementById("save-order-btn"); - if (saveBtn) { - saveBtn.addEventListener("click", function (e) { - e.preventDefault(); - self._saveOrderDraft(); - }); - } - - // Confirm order button - var confirmBtn = document.getElementById("confirm-order-btn"); - if (confirmBtn) { - confirmBtn.addEventListener("click", function (e) { - e.preventDefault(); - self._confirmOrder(); - }); + if (savCartBtn) { + savCartBtn.addEventListener("click", function (e) { + console.log("[CLICK] save-cart-btn clicked"); + e.preventDefault(); + self._saveCartAsDraft(); + }); + } } }, @@ -1475,7 +1502,7 @@ }, _saveOrderDraft: function () { - console.log("Saving order as draft:", this.orderId); + console.log("[_saveOrderDraft] Starting - this.orderId:", this.orderId); var self = this; var items = []; @@ -1979,13 +2006,10 @@ console.log("cart-items-container found:", !!cartContainer); console.log("confirm-order-btn found:", !!confirmBtn); - if (cartContainer || confirmBtn) { - console.log("Calling init()"); - var result = window.groupOrderShop.init(); - console.log("init() result:", result); - } else { - console.warn("No elements found to initialize cart"); - } + // Always initialize - it handles both cart pages and checkout pages + console.log("Calling init()"); + var result = window.groupOrderShop.init(); + console.log("init() result:", result); }); // Handle confirm order buttons in portal (My Orders page) diff --git a/website_sale_aplicoop/views/website_templates.xml b/website_sale_aplicoop/views/website_templates.xml index 1d135ba..4a89ee7 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -473,6 +473,14 @@
+
@@ -551,7 +559,18 @@
- + +
+
+ +
@@ -578,17 +597,6 @@
- - -
-
@@ -657,19 +665,9 @@ - - - - - -