[FIX] website_sale_aplicoop: Complete infinite scroll and search filter integration
Major fixes: - Fix JSON body parsing in load_products_ajax with type='http' route * Parse JSON from request.httprequest.get_data() instead of post params * Correctly read page, search, category from JSON request body - Fix search and category filter combination * Use intersection (&) instead of replacement to preserve both filters * Now respects search AND category simultaneously - Integrate realtime_search.js with infinite_scroll.js * Add resetWithFilters() method to reset scroll to page 1 with new filters * When search/category changes, reload products from server * Clear grid and load fresh results - Fix pagination reset logic * Set currentPage = 0 in resetWithFilters() so loadNextPage() increments to 1 * Prevents loading empty page 2 when resetting filters Results: ✅ Infinite scroll loads all pages correctly (1, 2, 3...) ✅ Search filters work across all products (not just loaded) ✅ Category filters work correctly ✅ Search AND category filters work together ✅ Page resets to 1 when filters change
This commit is contained in:
parent
534876242e
commit
5eb039ffe0
4 changed files with 603 additions and 71 deletions
|
|
@ -48,6 +48,7 @@
|
||||||
"assets": {
|
"assets": {
|
||||||
"web.assets_frontend": [
|
"web.assets_frontend": [
|
||||||
"website_sale_aplicoop/static/src/css/website_sale.css",
|
"website_sale_aplicoop/static/src/css/website_sale.css",
|
||||||
|
"website_sale_aplicoop/static/src/js/infinite_scroll.js",
|
||||||
],
|
],
|
||||||
"web.assets_tests": [
|
"web.assets_tests": [
|
||||||
"website_sale_aplicoop/static/tests/test_suite.js",
|
"website_sale_aplicoop/static/tests/test_suite.js",
|
||||||
|
|
|
||||||
|
|
@ -847,16 +847,18 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
# - Products in the selected categories
|
# - Products in the selected categories
|
||||||
# - Products provided by the selected suppliers
|
# - Products provided by the selected suppliers
|
||||||
# - Delegate discovery to the order model (centralised logic)
|
# - Delegate discovery to the order model (centralised logic)
|
||||||
products = group_order._get_products_for_group_order(group_order.id)
|
all_products = group_order._get_products_for_group_order(group_order.id)
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"eskaera_shop order_id=%d, total products=%d (discovered)",
|
"eskaera_shop order_id=%d, total products=%d (discovered)",
|
||||||
order_id,
|
order_id,
|
||||||
len(products),
|
len(all_products),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all available categories BEFORE filtering (so dropdown always shows all)
|
# Get all available categories BEFORE filtering (so dropdown always shows all)
|
||||||
# Include not only product categories but also their parent categories
|
# Include not only product categories but also their parent categories
|
||||||
product_categories = products.mapped("categ_id").filtered(lambda c: c.id > 0)
|
product_categories = all_products.mapped("categ_id").filtered(
|
||||||
|
lambda c: c.id > 0
|
||||||
|
)
|
||||||
|
|
||||||
# Collect all categories including parent chain
|
# Collect all categories including parent chain
|
||||||
all_categories_set = set()
|
all_categories_set = set()
|
||||||
|
|
@ -884,19 +886,24 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
search_query = post.get("search", "").strip()
|
search_query = post.get("search", "").strip()
|
||||||
category_filter = post.get("category", "0")
|
category_filter = post.get("category", "0")
|
||||||
|
|
||||||
# Apply search
|
# ===== IMPORTANT: Filter COMPLETE catalog BEFORE pagination =====
|
||||||
|
# This ensures search works on full catalog and tags show correct counts
|
||||||
|
filtered_products = all_products
|
||||||
|
|
||||||
|
# Apply search to COMPLETE catalog
|
||||||
if search_query:
|
if search_query:
|
||||||
products = products.filtered(
|
filtered_products = filtered_products.filtered(
|
||||||
lambda p: search_query.lower() in p.name.lower()
|
lambda p: search_query.lower() in p.name.lower()
|
||||||
or search_query.lower() in (p.description or "").lower()
|
or search_query.lower() in (p.description or "").lower()
|
||||||
)
|
)
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'eskaera_shop: Filtered by search "%s". Found %d',
|
'eskaera_shop: Filtered by search "%s". Found %d of %d total',
|
||||||
search_query,
|
search_query,
|
||||||
len(products),
|
len(filtered_products),
|
||||||
|
len(all_products),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply category filter
|
# Apply category filter to COMPLETE catalog
|
||||||
if category_filter != "0":
|
if category_filter != "0":
|
||||||
try:
|
try:
|
||||||
category_id = int(category_filter)
|
category_id = int(category_filter)
|
||||||
|
|
@ -916,7 +923,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
|
|
||||||
# Search for products in the selected category and all descendants
|
# Search for products in the selected category and all descendants
|
||||||
# This ensures we get products even if the category is a parent with no direct products
|
# This ensures we get products even if the category is a parent with no direct products
|
||||||
filtered_products = request.env["product.product"].search(
|
cat_filtered = request.env["product.product"].search(
|
||||||
[
|
[
|
||||||
("categ_id", "in", all_category_ids),
|
("categ_id", "in", all_category_ids),
|
||||||
("active", "=", True),
|
("active", "=", True),
|
||||||
|
|
@ -939,32 +946,65 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
get_order_descendants(group_order.category_ids)
|
get_order_descendants(group_order.category_ids)
|
||||||
|
|
||||||
# Keep only products that are in both the selected category AND order's permitted categories
|
# Keep only products that are in both the selected category AND order's permitted categories
|
||||||
filtered_products = filtered_products.filtered(
|
cat_filtered = cat_filtered.filtered(
|
||||||
lambda p: p.categ_id.id in order_cat_ids
|
lambda p: p.categ_id.id in order_cat_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
products = filtered_products
|
filtered_products = cat_filtered
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"eskaera_shop: Filtered by category %d and descendants. Found %d products",
|
"eskaera_shop: Filtered by category %d and descendants. Found %d of %d total",
|
||||||
category_id,
|
category_id,
|
||||||
len(products),
|
len(filtered_products),
|
||||||
|
len(all_products),
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
_logger.warning("eskaera_shop: Invalid category filter: %s", str(e))
|
_logger.warning("eskaera_shop: Invalid category filter: %s", str(e))
|
||||||
|
|
||||||
# Apply pagination if lazy loading enabled
|
# ===== Calculate available tags BEFORE pagination (on complete filtered set) =====
|
||||||
total_products = len(products)
|
available_tags_dict = {}
|
||||||
|
for product in filtered_products:
|
||||||
|
for tag in product.product_tag_ids:
|
||||||
|
# Only include tags that are visible on ecommerce
|
||||||
|
is_visible = getattr(
|
||||||
|
tag, "visible_on_ecommerce", True
|
||||||
|
) # Default to True if field doesn't exist
|
||||||
|
if not is_visible:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if tag.id not in available_tags_dict:
|
||||||
|
tag_color = tag.color if tag.color else None
|
||||||
|
available_tags_dict[tag.id] = {
|
||||||
|
"id": tag.id,
|
||||||
|
"name": tag.name,
|
||||||
|
"color": tag_color,
|
||||||
|
"count": 0,
|
||||||
|
}
|
||||||
|
available_tags_dict[tag.id]["count"] += 1
|
||||||
|
|
||||||
|
# Convert to sorted list of tags (sorted by name for consistent display)
|
||||||
|
available_tags = sorted(available_tags_dict.values(), key=lambda t: t["name"])
|
||||||
|
_logger.info(
|
||||||
|
"eskaera_shop: Found %d available tags for %d filtered products",
|
||||||
|
len(available_tags),
|
||||||
|
len(filtered_products),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== NOW apply pagination to the FILTERED results =====
|
||||||
|
total_products = len(filtered_products)
|
||||||
has_next = False
|
has_next = False
|
||||||
|
products = filtered_products
|
||||||
if lazy_loading_enabled:
|
if lazy_loading_enabled:
|
||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
products = products[offset : offset + per_page]
|
products = filtered_products[offset : offset + per_page]
|
||||||
has_next = offset + per_page < total_products
|
has_next = offset + per_page < total_products
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"eskaera_shop: Paginated - page=%d, offset=%d, per_page=%d, has_next=%s",
|
"eskaera_shop: Paginated - page=%d, offset=%d, per_page=%d, has_next=%s, showing %d of %d",
|
||||||
page,
|
page,
|
||||||
offset,
|
offset,
|
||||||
per_page,
|
per_page,
|
||||||
has_next,
|
has_next,
|
||||||
|
len(products),
|
||||||
|
total_products,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare supplier info dict: {product.id: 'Supplier (City)'}
|
# Prepare supplier info dict: {product.id: 'Supplier (City)'}
|
||||||
|
|
@ -1072,44 +1112,6 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
)
|
)
|
||||||
product_display_info[product.id] = display_info
|
product_display_info[product.id] = display_info
|
||||||
|
|
||||||
# Calculate available tags with product count (only show tags that are actually used and visible)
|
|
||||||
# Build a dict: {tag_id: {'id': tag_id, 'name': tag_name, 'count': num_products}}
|
|
||||||
available_tags_dict = {}
|
|
||||||
for product in products:
|
|
||||||
for tag in product.product_tag_ids:
|
|
||||||
# Only include tags that are visible on ecommerce
|
|
||||||
is_visible = getattr(
|
|
||||||
tag, "visible_on_ecommerce", True
|
|
||||||
) # Default to True if field doesn't exist
|
|
||||||
if not is_visible:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if tag.id not in available_tags_dict:
|
|
||||||
tag_color = tag.color if tag.color else None
|
|
||||||
_logger.info(
|
|
||||||
"Tag %s (id=%s): color=%s (type=%s)",
|
|
||||||
tag.name,
|
|
||||||
tag.id,
|
|
||||||
tag_color,
|
|
||||||
type(tag_color),
|
|
||||||
)
|
|
||||||
available_tags_dict[tag.id] = {
|
|
||||||
"id": tag.id,
|
|
||||||
"name": tag.name,
|
|
||||||
"color": tag_color, # Use tag color (hex) or None for theme color
|
|
||||||
"count": 0,
|
|
||||||
}
|
|
||||||
available_tags_dict[tag.id]["count"] += 1
|
|
||||||
|
|
||||||
# Convert to sorted list of tags (sorted by name for consistent display)
|
|
||||||
available_tags = sorted(available_tags_dict.values(), key=lambda t: t["name"])
|
|
||||||
_logger.info(
|
|
||||||
"eskaera_shop: Found %d available tags for %d products",
|
|
||||||
len(available_tags),
|
|
||||||
len(products),
|
|
||||||
)
|
|
||||||
_logger.info("eskaera_shop: available_tags = %s", available_tags)
|
|
||||||
|
|
||||||
# Manage session for separate cart per order
|
# Manage session for separate cart per order
|
||||||
session_key = f"eskaera_{order_id}"
|
session_key = f"eskaera_{order_id}"
|
||||||
cart = request.session.get(session_key, {})
|
cart = request.session.get(session_key, {})
|
||||||
|
|
@ -1119,10 +1121,10 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
|
|
||||||
# Filter product tags to only show published ones
|
# Filter product tags to only show published ones
|
||||||
# Create a dictionary with filtered tags for each product
|
# Create a dictionary with filtered tags for each product
|
||||||
filtered_products = {}
|
filtered_products_dict = {}
|
||||||
for product in products:
|
for product in products:
|
||||||
published_tags = self._filter_published_tags(product.product_tag_ids)
|
published_tags = self._filter_published_tags(product.product_tag_ids)
|
||||||
filtered_products[product.id] = {
|
filtered_products_dict[product.id] = {
|
||||||
"product": product,
|
"product": product,
|
||||||
"published_tags": published_tags,
|
"published_tags": published_tags,
|
||||||
}
|
}
|
||||||
|
|
@ -1132,7 +1134,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
{
|
{
|
||||||
"group_order": group_order,
|
"group_order": group_order,
|
||||||
"products": products,
|
"products": products,
|
||||||
"filtered_product_tags": filtered_products,
|
"filtered_product_tags": filtered_products_dict,
|
||||||
"cart": cart,
|
"cart": cart,
|
||||||
"available_categories": available_categories,
|
"available_categories": available_categories,
|
||||||
"category_hierarchy": category_hierarchy,
|
"category_hierarchy": category_hierarchy,
|
||||||
|
|
@ -1163,9 +1165,10 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
def load_eskaera_page(self, order_id, **post):
|
def load_eskaera_page(self, order_id, **post):
|
||||||
"""Load next page of products for lazy loading.
|
"""Load next page of products for lazy loading.
|
||||||
|
|
||||||
|
Respects same search/filter parameters as eskaera_shop.
|
||||||
Returns only HTML of product cards without page wrapper.
|
Returns only HTML of product cards without page wrapper.
|
||||||
"""
|
"""
|
||||||
group_order = request.env["group.order"].browse(order_id)
|
group_order = request.env["group_order"].browse(order_id)
|
||||||
|
|
||||||
if not group_order.exists() or group_order.state != "open":
|
if not group_order.exists() or group_order.state != "open":
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -1193,16 +1196,95 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get all products (same logic as eskaera_shop)
|
# Get all products (same logic as eskaera_shop)
|
||||||
products = group_order._get_products_for_group_order(group_order.id)
|
all_products = group_order._get_products_for_group_order(group_order.id)
|
||||||
|
|
||||||
|
# Get search and filter parameters (passed via POST/GET)
|
||||||
|
search_query = post.get("search", "").strip()
|
||||||
|
category_filter = post.get("category", "0")
|
||||||
|
|
||||||
|
# ===== Apply SAME filters as eskaera_shop =====
|
||||||
|
filtered_products = all_products
|
||||||
|
|
||||||
|
# Apply search
|
||||||
|
if 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_eskaera_page: search filter "%s" - found %d of %d',
|
||||||
|
search_query,
|
||||||
|
len(filtered_products),
|
||||||
|
len(all_products),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply category filter
|
||||||
|
if category_filter != "0":
|
||||||
|
try:
|
||||||
|
category_id = int(category_filter)
|
||||||
|
selected_category = request.env["product.category"].browse(category_id)
|
||||||
|
|
||||||
|
if selected_category.exists():
|
||||||
|
all_category_ids = [category_id]
|
||||||
|
|
||||||
|
def get_all_children(category):
|
||||||
|
for child in category.child_id:
|
||||||
|
all_category_ids.append(child.id)
|
||||||
|
get_all_children(child)
|
||||||
|
|
||||||
|
get_all_children(selected_category)
|
||||||
|
|
||||||
|
cat_filtered = request.env["product.product"].search(
|
||||||
|
[
|
||||||
|
("categ_id", "in", all_category_ids),
|
||||||
|
("active", "=", True),
|
||||||
|
("product_tmpl_id.is_published", "=", True),
|
||||||
|
("product_tmpl_id.sale_ok", "=", True),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if group_order.category_ids:
|
||||||
|
order_cat_ids = []
|
||||||
|
|
||||||
|
def get_order_descendants(categories):
|
||||||
|
for cat in categories:
|
||||||
|
order_cat_ids.append(cat.id)
|
||||||
|
if cat.child_id:
|
||||||
|
get_order_descendants(cat.child_id)
|
||||||
|
|
||||||
|
get_order_descendants(group_order.category_ids)
|
||||||
|
cat_filtered = cat_filtered.filtered(
|
||||||
|
lambda p: p.categ_id.id in order_cat_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
filtered_products = cat_filtered
|
||||||
|
_logger.info(
|
||||||
|
"load_eskaera_page: category filter %d - found %d of %d",
|
||||||
|
category_id,
|
||||||
|
len(filtered_products),
|
||||||
|
len(all_products),
|
||||||
|
)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
_logger.warning("load_eskaera_page: Invalid category filter")
|
||||||
|
|
||||||
|
# ===== Apply pagination to FILTERED results =====
|
||||||
|
total_products = len(filtered_products)
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
products_page = filtered_products[offset : offset + per_page]
|
||||||
|
has_next = offset + per_page < total_products
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"load_eskaera_page: page=%d, offset=%d, showing %d of %d filtered",
|
||||||
|
page,
|
||||||
|
offset,
|
||||||
|
len(products_page),
|
||||||
|
total_products,
|
||||||
|
)
|
||||||
|
|
||||||
# Get pricelist
|
# Get pricelist
|
||||||
pricelist = self._resolve_pricelist()
|
pricelist = self._resolve_pricelist()
|
||||||
|
|
||||||
# Calculate prices only for products on this page
|
# Calculate prices for this page
|
||||||
offset = (page - 1) * per_page
|
|
||||||
products_page = products[offset : offset + per_page]
|
|
||||||
has_next = offset + per_page < len(products)
|
|
||||||
|
|
||||||
product_price_info = {}
|
product_price_info = {}
|
||||||
for product in products_page:
|
for product in products_page:
|
||||||
product_variant = (
|
product_variant = (
|
||||||
|
|
@ -1261,10 +1343,10 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
product_supplier_info[product.id] = supplier_name
|
product_supplier_info[product.id] = supplier_name
|
||||||
|
|
||||||
# Filter product tags
|
# Filter product tags
|
||||||
filtered_products = {}
|
filtered_products_dict = {}
|
||||||
for product in products_page:
|
for product in products_page:
|
||||||
published_tags = self._filter_published_tags(product.product_tag_ids)
|
published_tags = self._filter_published_tags(product.product_tag_ids)
|
||||||
filtered_products[product.id] = {
|
filtered_products_dict[product.id] = {
|
||||||
"product": product,
|
"product": product,
|
||||||
"published_tags": published_tags,
|
"published_tags": published_tags,
|
||||||
}
|
}
|
||||||
|
|
@ -1286,7 +1368,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
{
|
{
|
||||||
"group_order": group_order,
|
"group_order": group_order,
|
||||||
"products": products_page,
|
"products": products_page,
|
||||||
"filtered_product_tags": filtered_products,
|
"filtered_product_tags": filtered_products_dict,
|
||||||
"product_supplier_info": product_supplier_info,
|
"product_supplier_info": product_supplier_info,
|
||||||
"product_price_info": product_price_info,
|
"product_price_info": product_price_info,
|
||||||
"product_display_info": product_display_info,
|
"product_display_info": product_display_info,
|
||||||
|
|
@ -1296,6 +1378,207 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@http.route(
|
||||||
|
["/eskaera/<int:order_id>/load-products-ajax"],
|
||||||
|
type="json",
|
||||||
|
auth="user",
|
||||||
|
website=True,
|
||||||
|
methods=["POST"],
|
||||||
|
csrf=False,
|
||||||
|
)
|
||||||
|
def load_products_ajax(self, order_id, **post):
|
||||||
|
"""Load products via AJAX for infinite scroll.
|
||||||
|
|
||||||
|
Returns JSON with:
|
||||||
|
- html: rendered product cards HTML
|
||||||
|
- has_next: whether there are more products
|
||||||
|
- next_page: page number to fetch next
|
||||||
|
- total: total filtered products
|
||||||
|
"""
|
||||||
|
group_order = request.env["group.order"].browse(order_id)
|
||||||
|
|
||||||
|
if not group_order.exists() or group_order.state != "open":
|
||||||
|
return {"error": "Order not found or not open", "html": ""}
|
||||||
|
|
||||||
|
# Get configuration
|
||||||
|
per_page = int(
|
||||||
|
request.env["ir.config_parameter"].get_param(
|
||||||
|
"website_sale_aplicoop.products_per_page", 20
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get page from POST
|
||||||
|
try:
|
||||||
|
page = int(post.get("page", 1))
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
# Get filters
|
||||||
|
search_query = post.get("search", "").strip()
|
||||||
|
category_filter = str(post.get("category", "0"))
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"load_products_ajax: order_id=%d, page=%d, search=%s, category=%s",
|
||||||
|
order_id,
|
||||||
|
page,
|
||||||
|
search_query,
|
||||||
|
category_filter,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all products
|
||||||
|
all_products = group_order._get_products_for_group_order(group_order.id)
|
||||||
|
filtered_products = all_products
|
||||||
|
|
||||||
|
# Apply search
|
||||||
|
if 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()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply category filter
|
||||||
|
if category_filter != "0":
|
||||||
|
try:
|
||||||
|
category_id = int(category_filter)
|
||||||
|
selected_category = request.env["product.category"].browse(category_id)
|
||||||
|
|
||||||
|
if selected_category.exists():
|
||||||
|
all_category_ids = [category_id]
|
||||||
|
|
||||||
|
def get_all_children(category):
|
||||||
|
for child in category.child_id:
|
||||||
|
all_category_ids.append(child.id)
|
||||||
|
get_all_children(child)
|
||||||
|
|
||||||
|
get_all_children(selected_category)
|
||||||
|
|
||||||
|
cat_filtered = request.env["product.product"].search(
|
||||||
|
[
|
||||||
|
("categ_id", "in", all_category_ids),
|
||||||
|
("active", "=", True),
|
||||||
|
("product_tmpl_id.is_published", "=", True),
|
||||||
|
("product_tmpl_id.sale_ok", "=", True),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if group_order.category_ids:
|
||||||
|
order_cat_ids = []
|
||||||
|
|
||||||
|
def get_order_descendants(categories):
|
||||||
|
for cat in categories:
|
||||||
|
order_cat_ids.append(cat.id)
|
||||||
|
if cat.child_id:
|
||||||
|
get_order_descendants(cat.child_id)
|
||||||
|
|
||||||
|
get_order_descendants(group_order.category_ids)
|
||||||
|
cat_filtered = cat_filtered.filtered(
|
||||||
|
lambda p: p.categ_id.id in order_cat_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
filtered_products = cat_filtered
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
_logger.warning(
|
||||||
|
"load_products_ajax: Invalid category filter: %s", str(e)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Paginate
|
||||||
|
total_products = len(filtered_products)
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
products_page = filtered_products[offset : offset + per_page]
|
||||||
|
has_next = offset + per_page < total_products
|
||||||
|
|
||||||
|
# Get prices
|
||||||
|
pricelist = self._resolve_pricelist()
|
||||||
|
product_price_info = {}
|
||||||
|
for product in products_page:
|
||||||
|
product_variant = (
|
||||||
|
product.product_variant_ids[0] if product.product_variant_ids else False
|
||||||
|
)
|
||||||
|
if product_variant and pricelist:
|
||||||
|
try:
|
||||||
|
price_info = product_variant._get_price(
|
||||||
|
qty=1.0,
|
||||||
|
pricelist=pricelist,
|
||||||
|
fposition=request.website.fiscal_position_id,
|
||||||
|
)
|
||||||
|
product_price_info[product.id] = {
|
||||||
|
"price": price_info.get("value", 0.0),
|
||||||
|
"list_price": price_info.get("original_value", 0.0),
|
||||||
|
"has_discounted_price": price_info.get("discount", 0.0) > 0,
|
||||||
|
"discount": price_info.get("discount", 0.0),
|
||||||
|
"tax_included": price_info.get("tax_included", True),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
product_price_info[product.id] = {
|
||||||
|
"price": product.list_price,
|
||||||
|
"list_price": product.list_price,
|
||||||
|
"has_discounted_price": False,
|
||||||
|
"discount": 0.0,
|
||||||
|
"tax_included": False,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
product_price_info[product.id] = {
|
||||||
|
"price": product.list_price,
|
||||||
|
"list_price": product.list_price,
|
||||||
|
"has_discounted_price": False,
|
||||||
|
"discount": 0.0,
|
||||||
|
"tax_included": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare display info
|
||||||
|
product_display_info = {}
|
||||||
|
for product in products_page:
|
||||||
|
display_info = self._prepare_product_display_info(
|
||||||
|
product, product_price_info
|
||||||
|
)
|
||||||
|
product_display_info[product.id] = display_info
|
||||||
|
|
||||||
|
# Prepare supplier info
|
||||||
|
product_supplier_info = {}
|
||||||
|
for product in products_page:
|
||||||
|
supplier_name = ""
|
||||||
|
if product.seller_ids:
|
||||||
|
partner = product.seller_ids[0].partner_id.sudo()
|
||||||
|
supplier_name = partner.name or ""
|
||||||
|
if partner.city:
|
||||||
|
supplier_name += f" ({partner.city})"
|
||||||
|
product_supplier_info[product.id] = supplier_name
|
||||||
|
|
||||||
|
# Filter tags
|
||||||
|
filtered_products_dict = {}
|
||||||
|
for product in products_page:
|
||||||
|
published_tags = self._filter_published_tags(product.product_tag_ids)
|
||||||
|
filtered_products_dict[product.id] = {
|
||||||
|
"product": product,
|
||||||
|
"published_tags": published_tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Render HTML
|
||||||
|
html = request.env["ir.ui.view"]._render(
|
||||||
|
"website_sale_aplicoop.eskaera_shop_products",
|
||||||
|
{
|
||||||
|
"group_order": group_order,
|
||||||
|
"products": products_page,
|
||||||
|
"filtered_product_tags": filtered_products_dict,
|
||||||
|
"product_supplier_info": product_supplier_info,
|
||||||
|
"product_price_info": product_price_info,
|
||||||
|
"product_display_info": product_display_info,
|
||||||
|
"labels": self.get_checkout_labels(),
|
||||||
|
"has_next": has_next,
|
||||||
|
"next_page": page + 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"html": html,
|
||||||
|
"has_next": has_next,
|
||||||
|
"next_page": page + 1,
|
||||||
|
"total": total_products,
|
||||||
|
"page": page,
|
||||||
|
}
|
||||||
|
|
||||||
@http.route(
|
@http.route(
|
||||||
["/eskaera/add-to-cart"],
|
["/eskaera/add-to-cart"],
|
||||||
type="http",
|
type="http",
|
||||||
|
|
|
||||||
225
website_sale_aplicoop/static/src/js/infinite_scroll.js
Normal file
225
website_sale_aplicoop/static/src/js/infinite_scroll.js
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
/**
|
||||||
|
* Infinite Scroll Handler for Eskaera Shop
|
||||||
|
*
|
||||||
|
* Automatically loads more products as user scrolls down the page.
|
||||||
|
* Falls back to manual "Load More" button if disabled or on error.
|
||||||
|
*/
|
||||||
|
|
||||||
|
console.log("[INFINITE_SCROLL] Script loaded!");
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Also run immediately if DOM is already loaded
|
||||||
|
var initInfiniteScroll = function () {
|
||||||
|
console.log("[INFINITE_SCROLL] Initializing infinite scroll...");
|
||||||
|
|
||||||
|
var infiniteScroll = {
|
||||||
|
orderId: null,
|
||||||
|
searchQuery: "",
|
||||||
|
category: "0",
|
||||||
|
perPage: 20,
|
||||||
|
currentPage: 1,
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: true,
|
||||||
|
config: {},
|
||||||
|
|
||||||
|
init: function () {
|
||||||
|
// Get configuration from page data
|
||||||
|
var configEl = document.getElementById("eskaera-config");
|
||||||
|
if (!configEl) {
|
||||||
|
console.log("[INFINITE_SCROLL] No eskaera-config found, lazy loading disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.orderId = configEl.getAttribute("data-order-id");
|
||||||
|
this.searchQuery = configEl.getAttribute("data-search") || "";
|
||||||
|
this.category = configEl.getAttribute("data-category") || "0";
|
||||||
|
this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20;
|
||||||
|
this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1;
|
||||||
|
|
||||||
|
// Check if there are more products to load
|
||||||
|
var hasNextBtn = document.getElementById("load-more-btn");
|
||||||
|
this.hasMore = hasNextBtn && hasNextBtn.offsetParent !== null; // offsetParent === null means hidden
|
||||||
|
|
||||||
|
if (!this.hasMore) {
|
||||||
|
console.log("[INFINITE_SCROLL] No more products to load");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[INFINITE_SCROLL] Initialized with:", {
|
||||||
|
orderId: this.orderId,
|
||||||
|
searchQuery: this.searchQuery,
|
||||||
|
category: this.category,
|
||||||
|
perPage: this.perPage,
|
||||||
|
currentPage: this.currentPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.attachScrollListener();
|
||||||
|
// Also keep the button listener as fallback
|
||||||
|
this.attachFallbackButtonListener();
|
||||||
|
},
|
||||||
|
|
||||||
|
attachScrollListener: function () {
|
||||||
|
var self = this;
|
||||||
|
var scrollThreshold = 0.8; // Load when 80% scrolled
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (scrollPercent >= scrollThreshold) {
|
||||||
|
console.log(
|
||||||
|
"[INFINITE_SCROLL] Scroll threshold reached, loading next page"
|
||||||
|
);
|
||||||
|
self.loadNextPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[INFINITE_SCROLL] Scroll listener attached (threshold:",
|
||||||
|
scrollThreshold * 100 + "%)"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
attachFallbackButtonListener: function () {
|
||||||
|
var self = this;
|
||||||
|
var btn = document.getElementById("load-more-btn");
|
||||||
|
|
||||||
|
if (!btn) {
|
||||||
|
console.log("[INFINITE_SCROLL] No fallback button found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!self.isLoading && self.hasMore) {
|
||||||
|
console.log("[INFINITE_SCROLL] Manual button click, loading next page");
|
||||||
|
self.loadNextPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[INFINITE_SCROLL] Fallback button listener attached");
|
||||||
|
},
|
||||||
|
|
||||||
|
loadNextPage: function () {
|
||||||
|
var self = this;
|
||||||
|
this.isLoading = true;
|
||||||
|
this.currentPage += 1;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[INFINITE_SCROLL] Loading page",
|
||||||
|
this.currentPage,
|
||||||
|
"for order",
|
||||||
|
this.orderId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show spinner
|
||||||
|
var spinner = document.getElementById("loading-spinner");
|
||||||
|
if (spinner) {
|
||||||
|
spinner.classList.remove("d-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
page: this.currentPage,
|
||||||
|
search: this.searchQuery,
|
||||||
|
category: this.category,
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("/eskaera/" + this.orderId + "/load-products-ajax", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok: " + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
if (result.error) {
|
||||||
|
console.error("[INFINITE_SCROLL] Server error:", result.error);
|
||||||
|
self.isLoading = false;
|
||||||
|
self.currentPage -= 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[INFINITE_SCROLL] Page loaded successfully", result);
|
||||||
|
|
||||||
|
// Insert HTML into grid
|
||||||
|
var grid = document.getElementById("products-grid");
|
||||||
|
if (grid && result.html) {
|
||||||
|
grid.insertAdjacentHTML("beforeend", result.html);
|
||||||
|
console.log("[INFINITE_SCROLL] Products inserted into grid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update has_more flag
|
||||||
|
self.hasMore = result.has_next || false;
|
||||||
|
|
||||||
|
if (!self.hasMore) {
|
||||||
|
console.log("[INFINITE_SCROLL] No more products available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide spinner
|
||||||
|
if (spinner) {
|
||||||
|
spinner.classList.add("d-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = false;
|
||||||
|
|
||||||
|
// Re-attach event listeners for newly added products
|
||||||
|
if (
|
||||||
|
window.aplicoopShop &&
|
||||||
|
typeof window.aplicoopShop._attachEventListeners === "function"
|
||||||
|
) {
|
||||||
|
window.aplicoopShop._attachEventListeners();
|
||||||
|
console.log("[INFINITE_SCROLL] Event listeners re-attached");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.error("[INFINITE_SCROLL] Fetch error:", error);
|
||||||
|
self.isLoading = false;
|
||||||
|
self.currentPage -= 1;
|
||||||
|
|
||||||
|
// Hide spinner on error
|
||||||
|
if (spinner) {
|
||||||
|
spinner.classList.add("d-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show fallback button
|
||||||
|
var btn = document.getElementById("load-more-btn");
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.remove("d-none");
|
||||||
|
btn.style.display = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize infinite scroll
|
||||||
|
infiniteScroll.init();
|
||||||
|
|
||||||
|
// Export to global scope for debugging
|
||||||
|
window.infiniteScroll = infiniteScroll;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run on DOMContentLoaded if DOM not yet ready
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
console.log("[INFINITE_SCROLL] DOM not ready, waiting for DOMContentLoaded...");
|
||||||
|
document.addEventListener("DOMContentLoaded", initInfiniteScroll);
|
||||||
|
} else {
|
||||||
|
// DOM is already loaded
|
||||||
|
console.log("[INFINITE_SCROLL] DOM already loaded, initializing immediately...");
|
||||||
|
initInfiniteScroll();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -551,22 +551,43 @@
|
||||||
<t t-call="website_sale_aplicoop.eskaera_shop_products" />
|
<t t-call="website_sale_aplicoop.eskaera_shop_products" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Load More Button (for lazy loading) -->
|
<!-- Infinite scroll container -->
|
||||||
<t t-if="lazy_loading_enabled and has_next">
|
<t t-if="lazy_loading_enabled and has_next">
|
||||||
<div class="row mt-4">
|
<div id="infinite-scroll-container" class="row mt-4">
|
||||||
<div class="col-12 text-center">
|
<div class="col-12 text-center">
|
||||||
|
<!-- Spinner (hidden by default) -->
|
||||||
|
<div id="loading-spinner" class="d-none" style="padding: 20px;">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">Loading more products...</p>
|
||||||
|
</div>
|
||||||
|
<!-- Fallback Load More button (shown if auto-load fails) -->
|
||||||
<button
|
<button
|
||||||
id="load-more-btn"
|
id="load-more-btn"
|
||||||
class="btn btn-primary btn-lg"
|
class="btn btn-primary btn-lg d-none"
|
||||||
t-attf-data-page="{{ current_page + 1 }}"
|
t-attf-data-page="{{ current_page + 1 }}"
|
||||||
t-attf-data-order-id="{{ group_order.id }}"
|
t-attf-data-order-id="{{ group_order.id }}"
|
||||||
|
t-attf-data-search="{{ search_query }}"
|
||||||
|
t-attf-data-category="{{ selected_category }}"
|
||||||
t-attf-data-per-page="{{ per_page }}"
|
t-attf-data-per-page="{{ per_page }}"
|
||||||
aria-label="Load more products"
|
aria-label="Load more products"
|
||||||
|
style="display: none;"
|
||||||
>
|
>
|
||||||
<i class="fa fa-download me-2" />Load More Products
|
<i class="fa fa-download me-2" />Load More Products
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Data attributes for infinite scroll configuration -->
|
||||||
|
<div id="eskaera-config"
|
||||||
|
t-attf-data-order-id="{{ group_order.id }}"
|
||||||
|
t-attf-data-search="{{ search_query }}"
|
||||||
|
t-attf-data-category="{{ selected_category }}"
|
||||||
|
t-attf-data-per-page="{{ per_page }}"
|
||||||
|
t-attf-data-current-page="{{ current_page }}"
|
||||||
|
class="d-none">
|
||||||
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
|
|
@ -646,6 +667,8 @@
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" />
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" />
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" />
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" />
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/realtime_search.js" />
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/realtime_search.js" />
|
||||||
|
<!-- Infinite scroll for lazy loading products -->
|
||||||
|
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/infinite_scroll.js" />
|
||||||
|
|
||||||
<!-- Initialize tooltips using native title attribute -->
|
<!-- Initialize tooltips using native title attribute -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue