From 5c328010f389a860ead85ea2f6f85c884c08bf12 Mon Sep 17 00:00:00 2001 From: MohamedAbdulsalam96 Date: Tue, 13 May 2025 12:28:39 +0200 Subject: [PATCH 1/9] Addition: Product search results display page --- webshop/api/__init__.py | 0 webshop/api/product_search.py | 24 ++++++ webshop/hooks.py | 7 +- webshop/public/js/product_ui/views.js | 11 ++- webshop/public/js/search.js | 65 +++++++++++++++++ webshop/webshop/api.py | 2 +- webshop/www/search.html | 101 ++++++++++++++++++++++++++ 7 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 webshop/api/__init__.py create mode 100644 webshop/api/product_search.py create mode 100644 webshop/public/js/search.js create mode 100644 webshop/www/search.html diff --git a/webshop/api/__init__.py b/webshop/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webshop/api/product_search.py b/webshop/api/product_search.py new file mode 100644 index 0000000000..02da334c2c --- /dev/null +++ b/webshop/api/product_search.py @@ -0,0 +1,24 @@ +# apps/webshop/webshop/api/product_search.py + +import frappe +from frappe import _ + +@frappe.whitelist() +def product_search(q): + if not q: + return [] + + or_filters = [ + ["item_code", "like", f"%{q}%"], + ["item_name", "like", f"%{q}%"], + ["web_long_description", "like", f"%{q}%"], + ["web_item_name", "like", f"%{q}%"] + ] + + return frappe.get_all( + "Website Item", + fields=["name", "item_code", "item_name", "item_group", "website_image", "web_item_name", "route"], + or_filters=or_filters, + filters={"published": 1}, + limit_page_length=20 + ) \ No newline at end of file diff --git a/webshop/hooks.py b/webshop/hooks.py index ed854d07cd..5b9db1cd36 100644 --- a/webshop/hooks.py +++ b/webshop/hooks.py @@ -14,6 +14,10 @@ web_include_js = "web.bundle.js" +app_include_js = [ + "/assets/webshop/js/search.js" +] + after_install = "webshop.setup.install.after_install" on_logout = "webshop.webshop.shopping_cart.utils.clear_cart_count" on_session_creation = [ @@ -23,6 +27,7 @@ update_website_context = [ "webshop.webshop.shopping_cart.utils.update_website_context", ] +my_account_context = "webshop.webshop.shopping_cart.utils.update_my_account_context" website_generators = ["Website Item", "Item Group"] @@ -75,4 +80,4 @@ has_website_permission = { "Website Item": "webshop.webshop.doctype.website_item.website_item.has_website_permission_for_website_item", "Item Group": "webshop.webshop.doctype.website_item.website_item.has_website_permission_for_item_group" -} +} \ No newline at end of file diff --git a/webshop/public/js/product_ui/views.js b/webshop/public/js/product_ui/views.js index 3863ff8c74..e65f45437a 100644 --- a/webshop/public/js/product_ui/views.js +++ b/webshop/public/js/product_ui/views.js @@ -204,6 +204,15 @@ webshop.ProductView = class { `); + $("#search-box").on("keypress", function (e) { + if (e.which === 13) { + e.preventDefault(); + const query = $(this).val().trim(); + if (query) { + window.location.href = `/search?q=${encodeURIComponent(query)}`; + } + } + }); } render_view_toggler() { @@ -545,4 +554,4 @@ webshop.ProductView = class { } return exists ? obj : undefined; } -}; +}; \ No newline at end of file diff --git a/webshop/public/js/search.js b/webshop/public/js/search.js new file mode 100644 index 0000000000..52e3fc90ed --- /dev/null +++ b/webshop/public/js/search.js @@ -0,0 +1,65 @@ +frappe.ready(() => { + const query = new URLSearchParams(window.location.search).get("q"); + if (!query) return; + + document.getElementById("search-query").textContent = query; + + const view_type = localStorage.getItem("product_view") || "Grid View"; + + // إزالة شريط البحث إن وجد + const searchInput = document.getElementById("search-bar"); + if (searchInput) searchInput.style.display = "none"; + + frappe.call({ + method: "webshop.api.product_search.product_search", + args: { q: query }, + callback: function (r) { + const results = r.message || []; + const container = document.getElementById("results"); + + if (!results.length) { + container.innerHTML = `

No products found.

`; + return; + } + + container.innerHTML = ""; + container.className = view_type === "List View" + ? "row list-view" + : "row grid-view"; + + for (let item of results) { + const title = item.web_item_name || item.item_name || "product"; + const image_url = item.website_image; + const product_url = item.route ? `/${item.route}` : `/${item.name}`; + const item_group = item.item_group || ""; + const initials = title.trim().substring(0, 2).toUpperCase(); + + const card = document.createElement("div"); + card.className = view_type === "List View" + ? "col-12 mb-3" + : "col-12 col-sm-6 col-md-4 col-lg-3 mb-4"; + + card.innerHTML = ` +
+ +
+ ${ + image_url + ? `${title}` + : `
${initials}
` + } +
+
+
${title}
+
+
+
${item_group}
+
+ `; + + container.appendChild(card); + + } + } + }); +}); diff --git a/webshop/webshop/api.py b/webshop/webshop/api.py index 82650d868d..861a9eb373 100644 --- a/webshop/webshop/api.py +++ b/webshop/webshop/api.py @@ -86,4 +86,4 @@ def get_product_filter_data(query_args=None): @frappe.whitelist(allow_guest=True) def get_guest_redirect_on_action(): - return frappe.db.get_single_value("Webshop Settings", "redirect_on_action") + return frappe.db.get_single_value("Webshop Settings", "redirect_on_action") \ No newline at end of file diff --git a/webshop/www/search.html b/webshop/www/search.html new file mode 100644 index 0000000000..fd07f101ef --- /dev/null +++ b/webshop/www/search.html @@ -0,0 +1,101 @@ +{% extends "templates/web.html" %} + +{% block title %}نتائج البحث{% endblock %} + +{% block page_content %} + + +
+

Search results for: ""

+
+
+ +{% endblock %} + +{% block script %} + +{% endblock %} \ No newline at end of file From 4adad70e5a8881812beb3d06ec62ce074759273c Mon Sep 17 00:00:00 2001 From: MohamedAbdulsalam96 Date: Fri, 16 May 2025 16:20:24 +0200 Subject: [PATCH 2/9] Better CSs --- webshop/hooks.py | 11 ++++++- webshop/public/css/custom_style.css | 10 ++++++ webshop/public/js/search.js | 45 ++++++++++++++------------- webshop/public/scss/webshop_cart.scss | 6 ++-- 4 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 webshop/public/css/custom_style.css diff --git a/webshop/hooks.py b/webshop/hooks.py index 5b9db1cd36..0c9d2cf8d5 100644 --- a/webshop/hooks.py +++ b/webshop/hooks.py @@ -10,10 +10,19 @@ required_apps = ["payments", "erpnext"] -web_include_css = "webshop-web.bundle.css" +# web_include_css = "webshop-web.bundle.css" + +web_include_css = [ + "webshop-web.bundle.css", + "/assets/webshop/css/custom_style.css" +] web_include_js = "web.bundle.js" +# app_include_css = [ +# "/assets/webshop/css/custom_style.css" +# ] + app_include_js = [ "/assets/webshop/js/search.js" ] diff --git a/webshop/public/css/custom_style.css b/webshop/public/css/custom_style.css new file mode 100644 index 0000000000..8422e1b859 --- /dev/null +++ b/webshop/public/css/custom_style.css @@ -0,0 +1,10 @@ +.page-header-wrapper { + display: flex; + justify-content: center; + align-items: center; +} + +.filters-section{ + background-color: white; + padding: 20px; +} \ No newline at end of file diff --git a/webshop/public/js/search.js b/webshop/public/js/search.js index 52e3fc90ed..ef04417e92 100644 --- a/webshop/public/js/search.js +++ b/webshop/public/js/search.js @@ -34,30 +34,31 @@ frappe.ready(() => { const item_group = item.item_group || ""; const initials = title.trim().substring(0, 2).toUpperCase(); - const card = document.createElement("div"); - card.className = view_type === "List View" - ? "col-12 mb-3" - : "col-12 col-sm-6 col-md-4 col-lg-3 mb-4"; + const card = document.createElement("div"); + card.className = view_type === "List View" + ? "col-12 mb-3" + : "col-12 col-sm-6 col-md-4 col-lg-3 mb-4"; - card.innerHTML = ` - - `; + card.innerHTML = ` +
+
+ ${ + image_url + ? `${title}` + : `
${initials}
` + } +
+
+ + ${title} + +
${item_group}
+
+
+ `; + + container.appendChild(card); - container.appendChild(card); } } diff --git a/webshop/public/scss/webshop_cart.scss b/webshop/public/scss/webshop_cart.scss index 9a49e1ba67..46f21eb1c6 100644 --- a/webshop/public/scss/webshop_cart.scss +++ b/webshop/public/scss/webshop_cart.scss @@ -1286,7 +1286,8 @@ body.product-page { #search-box { background-color: white; - height: 100%; + // height: 100%; + height: 150%; padding-left: 2.5rem; border: 1px solid var(--gray-200); } @@ -1294,7 +1295,8 @@ body.product-page { .search-icon { position: absolute; left: 0; - top: 0; + // top: 0; + top: 9px; width: 2.5rem; height: 100%; display: flex; From 3fa16690230b10e8583f36235fc83635072cf581 Mon Sep 17 00:00:00 2001 From: alaalsalam Date: Fri, 16 May 2025 19:12:56 +0200 Subject: [PATCH 3/9] fix get shipping rule from another company --- webshop/api/product_search.py | 3 +- webshop/templates/includes/macros.html | 2 +- webshop/webshop/shopping_cart/cart.py | 45 +++++++++++++++++--------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/webshop/api/product_search.py b/webshop/api/product_search.py index 02da334c2c..14596bff98 100644 --- a/webshop/api/product_search.py +++ b/webshop/api/product_search.py @@ -3,7 +3,8 @@ import frappe from frappe import _ -@frappe.whitelist() +# @frappe.whitelist() +@frappe.whitelist(allow_guest = True) def product_search(q): if not q: return [] diff --git a/webshop/templates/includes/macros.html b/webshop/templates/includes/macros.html index 913aa776a8..bda4176094 100644 --- a/webshop/templates/includes/macros.html +++ b/webshop/templates/includes/macros.html @@ -302,7 +302,7 @@

{% if values | len > 20 %} - + {% endif %} {% if values %} diff --git a/webshop/webshop/shopping_cart/cart.py b/webshop/webshop/shopping_cart/cart.py index bb2e2a5d4b..39f0dde93f 100644 --- a/webshop/webshop/shopping_cart/cart.py +++ b/webshop/webshop/shopping_cart/cart.py @@ -722,31 +722,44 @@ def get_applicable_shipping_rules(party=None, quotation=None): def get_shipping_rules(quotation=None, cart_settings=None): + webshop_settings = frappe.get_cached_doc("Webshop Settings") + if not quotation: quotation = _get_cart_quotation() + if not quotation: + return [] shipping_rules = [] - if quotation.shipping_address_name: - country = frappe.db.get_value( - "Address", quotation.shipping_address_name, "country" + if not quotation.shipping_address_name: + return [] + + country = frappe.db.get_value("Address", quotation.shipping_address_name, "country") + if not country: + return [] + + sr_country = frappe.qb.DocType("Shipping Rule Country") + sr = frappe.qb.DocType("Shipping Rule") + + query = ( + frappe.qb.from_(sr_country) + .join(sr) + .on(sr.name == sr_country.parent) + .select(sr.name) + .distinct() + .where( + (sr_country.country == country) + & (sr.disabled != 1) + & (sr.company == webshop_settings.company) ) - if country: - sr_country = frappe.qb.DocType("Shipping Rule Country") - sr = frappe.qb.DocType("Shipping Rule") - query = ( - frappe.qb.from_(sr_country) - .join(sr) - .on(sr.name == sr_country.parent) - .select(sr.name) - .distinct() - .where((sr_country.country == country) & (sr.disabled != 1)) - ) - result = query.run(as_list=True) - shipping_rules = [x[0] for x in result] + ) + + result = query.run() + shipping_rules = [x[0] for x in result] return shipping_rules + def get_address_territory(address_name): """Tries to match city, state and country of address to existing territory""" territory = None From 9c1ded53d2f96322532d2ad84c74bf5bf5c9f707 Mon Sep 17 00:00:00 2001 From: MohamedAbdulsalam96 Date: Sun, 18 May 2025 11:54:47 +0200 Subject: [PATCH 4/9] add stock valiabel --- webshop/api/product_search.py | 24 ------- webshop/api/search Temp.py | 23 +++++++ webshop/api/search.py | 52 ++++++++++++++++ webshop/public/js/product_ui/grid.js | 14 ++++- webshop/public/js/product_ui/list.js | 14 ++++- webshop/public/js/search Temp.js | 27 ++++++++ webshop/public/js/search.js | 36 ++++++++++- webshop/webshop/api.py | 93 +++++++++++++++++++++++++++- webshop/www/search Temp.html | 46 ++++++++++++++ 9 files changed, 297 insertions(+), 32 deletions(-) delete mode 100644 webshop/api/product_search.py create mode 100644 webshop/api/search Temp.py create mode 100644 webshop/api/search.py create mode 100644 webshop/public/js/search Temp.js create mode 100644 webshop/www/search Temp.html diff --git a/webshop/api/product_search.py b/webshop/api/product_search.py deleted file mode 100644 index 02da334c2c..0000000000 --- a/webshop/api/product_search.py +++ /dev/null @@ -1,24 +0,0 @@ -# apps/webshop/webshop/api/product_search.py - -import frappe -from frappe import _ - -@frappe.whitelist() -def product_search(q): - if not q: - return [] - - or_filters = [ - ["item_code", "like", f"%{q}%"], - ["item_name", "like", f"%{q}%"], - ["web_long_description", "like", f"%{q}%"], - ["web_item_name", "like", f"%{q}%"] - ] - - return frappe.get_all( - "Website Item", - fields=["name", "item_code", "item_name", "item_group", "website_image", "web_item_name", "route"], - or_filters=or_filters, - filters={"published": 1}, - limit_page_length=20 - ) \ No newline at end of file diff --git a/webshop/api/search Temp.py b/webshop/api/search Temp.py new file mode 100644 index 0000000000..3e52fe5a7a --- /dev/null +++ b/webshop/api/search Temp.py @@ -0,0 +1,23 @@ +import frappe +import json +from webshop.api.search import product_search +from webshop.api import get_webshop_settings + + +def get_context(context): + # query = frappe.form_dict.get("query") or "" + query = frappe.form_dict.get("query") or frappe.form_dict.get("q") or "" + + context.query = query + context.products = [] + context.webshop_settings = get_webshop_settings() + + if query: + context.products = product_search(query) + + # تمرير المنتجات كـ JSON إلى JavaScript مع حماية + context.products_json = json.dumps(context.products or [], default=str) + context.webshop_settings_json = json.dumps(context.webshop_settings or {}, default=str) + + return context + diff --git a/webshop/api/search.py b/webshop/api/search.py new file mode 100644 index 0000000000..4195469377 --- /dev/null +++ b/webshop/api/search.py @@ -0,0 +1,52 @@ +# apps/webshop/webshop/api/search.py + +import frappe +from frappe import _ +import json +from frappe.utils.data import flt + +@frappe.whitelist() +def product_search(q): + if not q: + return [] + + or_filters = [ + ["item_code", "like", f"%{q}%"], + ["item_name", "like", f"%{q}%"], + ["web_long_description", "like", f"%{q}%"], + ["web_item_name", "like", f"%{q}%"] + ] + + items = frappe.get_all( + "Website Item", + fields=["name", "item_code", "item_name", "item_group", "website_image", "web_item_name", "route"], + or_filters=or_filters, + filters={"published": 1}, + limit_page_length=20 + ) + + for item in items: + item_doc = frappe.get_doc("Item", item.item_code) + + # جلب المخزون من الـ Website Item نفسه أو إعدادات النظام + warehouse = frappe.db.get_value("Website Item", item.name, "website_warehouse") or \ + frappe.db.get_single_value("Stock Settings", "default_warehouse") + + # جلب الكمية المتوفرة + actual_qty = 0 + if item.item_code and warehouse: + actual_qty = frappe.db.get_value("Bin", { + "item_code": item.item_code, + "warehouse": warehouse + }, "actual_qty") or 0 + + # تحديث خصائص المنتج بالمخزون + item.update({ + "stock_qty": flt(actual_qty), + "in_stock": actual_qty > 0, + "is_stock": item_doc.is_stock_item, + "on_backorder": item_doc.delivered_by_supplier, + "has_variants": item_doc.has_variants + }) + + return items diff --git a/webshop/public/js/product_ui/grid.js b/webshop/public/js/product_ui/grid.js index 0212d823a2..8ff18961ef 100644 --- a/webshop/public/js/product_ui/grid.js +++ b/webshop/public/js/product_ui/grid.js @@ -1,3 +1,5 @@ +window.webshop = window.webshop || {}; + webshop.ProductGrid = class { /* Options: - items: Items @@ -155,12 +157,22 @@ webshop.ProductGrid = class { ${ __("Out of stock") } `; + } else { + let qty_display = `${ __("In stock") }`; + if (item.stock_qty) { + qty_display += ` (${item.stock_qty})`; + } + return ` + + ${qty_display} + + `; } } - return ``; } + get_primary_button(item, settings) { if (item.has_variants) { return ` diff --git a/webshop/public/js/product_ui/list.js b/webshop/public/js/product_ui/list.js index b4df29d8f2..58dbabdf30 100644 --- a/webshop/public/js/product_ui/list.js +++ b/webshop/public/js/product_ui/list.js @@ -1,3 +1,5 @@ +window.webshop = window.webshop || {}; + webshop.ProductList = class { /* Options: - items: Items @@ -138,17 +140,23 @@ webshop.ProductList = class {
${ __("Out of stock") } `; - } else if (item.is_stock) { + } else { + let qty_display = `${ __("In stock") }`; + if (item.stock_qty) { + qty_display += ` (${item.stock_qty})`; + } return `
- ${ __("In stock") } + + ${qty_display} + `; } } return ``; } + get_wishlist_icon(item) { let icon_class = item.wished ? "wished" : "not-wished"; diff --git a/webshop/public/js/search Temp.js b/webshop/public/js/search Temp.js new file mode 100644 index 0000000000..422e467a90 --- /dev/null +++ b/webshop/public/js/search Temp.js @@ -0,0 +1,27 @@ +frappe.ready(() => { + const products_section = $("#product-listing"); + const preference = "Grid View"; // يمكنك لاحقًا جعله ديناميكيًا حسب تفضيل المستخدم + const items = window.search_items || []; + const settings = window.webshop_settings || {}; + + if (items.length === 0) { + products_section.html(`

${__("لا توجد منتجات مطابقة.")}

`); + return; + } + + if (preference === "Grid View") { + new webshop.ProductGrid({ + items, + settings, + products_section, + preference, + }); + } else { + new webshop.ProductList({ + items, + settings, + products_section, + preference, + }); + } +}); diff --git a/webshop/public/js/search.js b/webshop/public/js/search.js index ef04417e92..168931b755 100644 --- a/webshop/public/js/search.js +++ b/webshop/public/js/search.js @@ -11,7 +11,7 @@ frappe.ready(() => { if (searchInput) searchInput.style.display = "none"; frappe.call({ - method: "webshop.api.product_search.product_search", + method: "webshop.api.search.product_search", args: { q: query }, callback: function (r) { const results = r.message || []; @@ -53,14 +53,44 @@ frappe.ready(() => { ${title}
${item_group}
+
+ ${get_stock_availability(item, { show_stock_availability: true })} +
`; container.appendChild(card); - - } } }); }); + +function get_stock_availability(item, settings) { + if (settings.show_stock_availability && !item.has_variants) { + if (item.on_backorder) { + return ` + + ${ __("Available on backorder") } + + `; + } else if (!item.in_stock) { + return ` + + ${ __("Out of stock") } + + `; + } else { + let qty_display = `${ __("In stock") }`; + if (item.stock_qty) { + qty_display += ` (${item.stock_qty})`; + } + return ` + + ${qty_display} + + `; + } + } + return ``; +} diff --git a/webshop/webshop/api.py b/webshop/webshop/api.py index 861a9eb373..dc39b914fa 100644 --- a/webshop/webshop/api.py +++ b/webshop/webshop/api.py @@ -5,13 +5,84 @@ import json import frappe -from frappe.utils import cint +from frappe.utils import cint, flt from webshop.webshop.product_data_engine.filters import ProductFiltersBuilder from webshop.webshop.product_data_engine.query import ProductQuery from webshop.webshop.doctype.override_doctype.item_group import get_child_groups_for_website +# @frappe.whitelist(allow_guest=True) +# def get_product_filter_data(query_args=None): +# """ +# Returns filtered products and discount filters. + +# Args: +# query_args (dict): contains filters to get products list + +# Query Args filters: +# search (str): Search Term. +# field_filters (dict): Keys include item_group, brand, etc. +# attribute_filters(dict): Keys include Color, Size, etc. +# start (int): Offset items by +# item_group (str): Valid Item Group +# from_filters (bool): Set as True to jump to page 1 +# """ +# if isinstance(query_args, str): +# query_args = json.loads(query_args) + +# query_args = frappe._dict(query_args or {}) + +# if query_args: +# search = query_args.get("search") +# field_filters = query_args.get("field_filters", {}) +# attribute_filters = query_args.get("attribute_filters", {}) +# start = cint(query_args.start) if query_args.get("start") else 0 +# item_group = query_args.get("item_group") +# from_filters = query_args.get("from_filters") +# else: +# search, attribute_filters, item_group, from_filters = None, None, None, None +# field_filters = {} +# start = 0 + +# # if new filter is checked, reset start to show filtered items from page 1 +# if from_filters: +# start = 0 + +# sub_categories = [] +# if item_group: +# sub_categories = get_child_groups_for_website(item_group, immediate=True) + +# engine = ProductQuery() + +# try: +# result = engine.query( +# attribute_filters, +# field_filters, +# search_term=search, +# start=start, +# item_group=item_group, +# ) +# except Exception: +# frappe.log_error("Product query with filter failed") +# return {"exc": "Something went wrong!"} + +# # discount filter data +# filters = {} +# discounts = result["discounts"] + +# if discounts: +# filter_engine = ProductFiltersBuilder() +# filters["discount_filters"] = filter_engine.get_discount_filters(discounts) + +# return { +# "items": result["items"] or [], +# "filters": filters, +# "settings": engine.settings, +# "sub_categories": sub_categories, +# "items_count": result["items_count"], +# } + @frappe.whitelist(allow_guest=True) def get_product_filter_data(query_args=None): """ @@ -75,6 +146,25 @@ def get_product_filter_data(query_args=None): filter_engine = ProductFiltersBuilder() filters["discount_filters"] = filter_engine.get_discount_filters(discounts) + for item in result["items"]: + item_code = item.get("item_code") + website_warehouse = item.get("website_warehouse") + + if not website_warehouse: + website_warehouse = frappe.db.get_value("Website Item", item.get("name"), "website_warehouse") + + actual_qty = 0 + if item_code and website_warehouse: + actual_qty = frappe.db.get_value("Bin", { + "item_code": item_code, + "warehouse": website_warehouse + }, "actual_qty") or 0 + + item["actual_qty"] = flt(actual_qty) + item["stock_qty"] = flt(actual_qty) + item["in_stock"] = actual_qty > 0 + + return { "items": result["items"] or [], "filters": filters, @@ -84,6 +174,7 @@ def get_product_filter_data(query_args=None): } + @frappe.whitelist(allow_guest=True) def get_guest_redirect_on_action(): return frappe.db.get_single_value("Webshop Settings", "redirect_on_action") \ No newline at end of file diff --git a/webshop/www/search Temp.html b/webshop/www/search Temp.html new file mode 100644 index 0000000000..827de5d2ef --- /dev/null +++ b/webshop/www/search Temp.html @@ -0,0 +1,46 @@ +{% extends "templates/web.html" %} +{% from "webshop/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %} + +{% block title %}{{ _("نتائج البحث") }}{% endblock %} + +{% block header %} +
{{ _("نتائج البحث عن: ") }} "{{ query or '' }}"
+{% endblock header %} + +{% block page_content %} +
+ +
+ +
+ + +
+
+
+
{{ _('Filters') }}
+ {{ _('Clear All') }} +
+ + {% if field_filters %} + {{ _(field_filter_section(field_filters)) }} + {% endif %} + + + {% if attribute_filters %} + {{ _(attribute_filter_section(attribute_filters)) }} + {% endif %} +
+
+
+ + + + + + + +{% endblock %} From ca65be103c86c26b3bb9c8c2e39a2806e356878d Mon Sep 17 00:00:00 2001 From: alaalsalam Date: Sun, 18 May 2025 17:49:38 +0200 Subject: [PATCH 5/9] fix delete website item error --- webshop/webshop/redisearch_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webshop/webshop/redisearch_utils.py b/webshop/webshop/redisearch_utils.py index 3549bc95eb..f52137f19f 100644 --- a/webshop/webshop/redisearch_utils.py +++ b/webshop/webshop/redisearch_utils.py @@ -158,7 +158,9 @@ def delete_item_from_index(website_item_doc): def delete_from_ac_dict(website_item_doc): """Removes this items's name from autocomplete dictionary""" ac = frappe.cache().ft() - ac.sugdel(website_item_doc.web_item_name) + # ac.sugdel(website_item_doc.web_item_name) + ac.sugdel("autocomplete_key", website_item_doc.web_item_name) + @if_redisearch_enabled From fe5181fe74c04cf92a8caf739e0337803aeee4dd Mon Sep 17 00:00:00 2001 From: alaalsalam Date: Mon, 19 May 2025 20:16:56 +0200 Subject: [PATCH 6/9] add Sort By to view product --- webshop/webshop/api.py | 189 ++++++++++++++---- .../webshop_settings/webshop_settings.json | 9 +- webshop/webshop/product_data_engine/query.py | 28 ++- 3 files changed, 182 insertions(+), 44 deletions(-) diff --git a/webshop/webshop/api.py b/webshop/webshop/api.py index dc39b914fa..8e7cbd6943 100644 --- a/webshop/webshop/api.py +++ b/webshop/webshop/api.py @@ -83,48 +83,154 @@ # "items_count": result["items_count"], # } +# ############################################################################################### + +# @frappe.whitelist(allow_guest=True) +# def get_product_filter_data(query_args=None): +# """ +# Returns filtered products and discount filters. + +# Args: +# query_args (dict): contains filters to get products list + +# Query Args filters: +# search (str): Search Term. +# field_filters (dict): Keys include item_group, brand, etc. +# attribute_filters(dict): Keys include Color, Size, etc. +# start (int): Offset items by +# item_group (str): Valid Item Group +# from_filters (bool): Set as True to jump to page 1 +# """ +# if isinstance(query_args, str): +# query_args = json.loads(query_args) + +# query_args = frappe._dict(query_args or {}) + +# if query_args: +# search = query_args.get("search") +# field_filters = query_args.get("field_filters", {}) +# attribute_filters = query_args.get("attribute_filters", {}) +# start = cint(query_args.start) if query_args.get("start") else 0 +# item_group = query_args.get("item_group") +# from_filters = query_args.get("from_filters") +# else: +# search, attribute_filters, item_group, from_filters = None, None, None, None +# field_filters = {} +# start = 0 + +# # if new filter is checked, reset start to show filtered items from page 1 +# if from_filters: +# start = 0 + +# sub_categories = [] +# if item_group: +# sub_categories = get_child_groups_for_website(item_group, immediate=True) + +# engine = ProductQuery() + +# try: +# result = engine.query( +# attribute_filters, +# field_filters, +# search_term=search, +# start=start, +# item_group=item_group, +# ) +# except Exception: +# frappe.log_error("Product query with filter failed") +# return {"exc": "Something went wrong!"} + +# # discount filter data +# filters = {} +# discounts = result["discounts"] + +# if discounts: +# filter_engine = ProductFiltersBuilder() +# filters["discount_filters"] = filter_engine.get_discount_filters(discounts) + +# for item in result["items"]: +# item_code = item.get("item_code") +# website_warehouse = item.get("website_warehouse") + +# if not website_warehouse: +# website_warehouse = frappe.db.get_value("Website Item", item.get("name"), "website_warehouse") + +# actual_qty = 0 +# if item_code and website_warehouse: +# actual_qty = frappe.db.get_value("Bin", { +# "item_code": item_code, +# "warehouse": website_warehouse +# }, "actual_qty") or 0 + +# item["actual_qty"] = flt(actual_qty) +# item["stock_qty"] = flt(actual_qty) +# item["in_stock"] = actual_qty > 0 + + +# return { +# "items": result["items"] or [], +# "filters": filters, +# "settings": engine.settings, +# "sub_categories": sub_categories, +# "items_count": result["items_count"], +# } + +# ############################################################################################### + +# @frappe.whitelist() +# def get_product_filter_data(start=0, search=None, item_group=None, fields=None, attributes=None): +# if isinstance(fields, str): +# fields = json.loads(fields) +# if isinstance(attributes, str): +# attributes = json.loads(attributes) + +# from webshop.webshop.product_data_engine.query import ProductQuery + +# # استخدم Webshop Settings لتحديد الترتيب +# settings = frappe.get_cached_doc("Webshop Settings") +# # sort_by = settings.sort_by or "ranking_desc" +# sort_by = settings.sort_by or "most_used" + + +# query_engine = ProductQuery() +# query_engine.set_sort_order(sort_by) + +# frappe.log_error("Using sort order: " + query_engine.sort_order) + +# return query_engine.query( +# attributes=attributes, +# fields=fields, +# search_term=search, +# start=start, +# item_group=item_group, +# ) + +# ############################################################################################### + @frappe.whitelist(allow_guest=True) def get_product_filter_data(query_args=None): - """ - Returns filtered products and discount filters. - - Args: - query_args (dict): contains filters to get products list - - Query Args filters: - search (str): Search Term. - field_filters (dict): Keys include item_group, brand, etc. - attribute_filters(dict): Keys include Color, Size, etc. - start (int): Offset items by - item_group (str): Valid Item Group - from_filters (bool): Set as True to jump to page 1 - """ if isinstance(query_args, str): query_args = json.loads(query_args) query_args = frappe._dict(query_args or {}) - if query_args: - search = query_args.get("search") - field_filters = query_args.get("field_filters", {}) - attribute_filters = query_args.get("attribute_filters", {}) - start = cint(query_args.start) if query_args.get("start") else 0 - item_group = query_args.get("item_group") - from_filters = query_args.get("from_filters") - else: - search, attribute_filters, item_group, from_filters = None, None, None, None - field_filters = {} - start = 0 + search = query_args.get("search") + field_filters = query_args.get("field_filters", {}) + attribute_filters = query_args.get("attribute_filters", {}) + start = cint(query_args.get("start") or 0) + item_group = query_args.get("item_group") + from_filters = query_args.get("from_filters") - # if new filter is checked, reset start to show filtered items from page 1 if from_filters: start = 0 - sub_categories = [] - if item_group: - sub_categories = get_child_groups_for_website(item_group, immediate=True) + settings = frappe.get_cached_doc("Webshop Settings") + sort_by = settings.sort_by or "most_used" engine = ProductQuery() + engine.set_sort_order(sort_by) + + frappe.log_error("Using sort order: " + engine.sort_order) try: result = engine.query( @@ -134,11 +240,10 @@ def get_product_filter_data(query_args=None): start=start, item_group=item_group, ) - except Exception: - frappe.log_error("Product query with filter failed") + except Exception as e: + frappe.log_error(f"Product query failed: {frappe.get_traceback()}") return {"exc": "Something went wrong!"} - # discount filter data filters = {} discounts = result["discounts"] @@ -148,22 +253,25 @@ def get_product_filter_data(query_args=None): for item in result["items"]: item_code = item.get("item_code") - website_warehouse = item.get("website_warehouse") - - if not website_warehouse: - website_warehouse = frappe.db.get_value("Website Item", item.get("name"), "website_warehouse") + website_warehouse = item.get("website_warehouse") or frappe.db.get_value( + "Website Item", item.get("name"), "website_warehouse" + ) actual_qty = 0 if item_code and website_warehouse: - actual_qty = frappe.db.get_value("Bin", { - "item_code": item_code, - "warehouse": website_warehouse - }, "actual_qty") or 0 + actual_qty = frappe.db.get_value( + "Bin", + {"item_code": item_code, "warehouse": website_warehouse}, + "actual_qty" + ) or 0 item["actual_qty"] = flt(actual_qty) item["stock_qty"] = flt(actual_qty) item["in_stock"] = actual_qty > 0 + sub_categories = [] + if item_group: + sub_categories = get_child_groups_for_website(item_group, immediate=True) return { "items": result["items"] or [], @@ -174,7 +282,6 @@ def get_product_filter_data(query_args=None): } - @frappe.whitelist(allow_guest=True) def get_guest_redirect_on_action(): return frappe.db.get_single_value("Webshop Settings", "redirect_on_action") \ No newline at end of file diff --git a/webshop/webshop/doctype/webshop_settings/webshop_settings.json b/webshop/webshop/doctype/webshop_settings/webshop_settings.json index 72df08643e..737a6e3d99 100644 --- a/webshop/webshop/doctype/webshop_settings/webshop_settings.json +++ b/webshop/webshop/doctype/webshop_settings/webshop_settings.json @@ -15,6 +15,7 @@ "hide_variants", "enable_variants", "show_price", + "sort_by", "column_break_9", "login_required_to_view_products", "show_stock_availability", @@ -390,13 +391,19 @@ "fieldname": "hide_unavailable_items", "fieldtype": "Check", "label": " Hide Unavailable Items" + }, + { + "fieldname": "sort_by", + "fieldtype": "Select", + "label": "Sort By", + "options": "\nLast Updated On Asc\nLast Updated On Desc\nWebsite Item Name Asc\nWebsite Item Name Desc\nID Asc\nID Desc\nCreated On Asc\nCreated On Desc\nMost Used\nRoute\nItem Code Asc\nItem Code Desc\nItem Group Asc\nItem Group Desc" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-18 17:46:31.931243", + "modified": "2025-05-19 13:38:51.119954", "modified_by": "Administrator", "module": "Webshop", "name": "Webshop Settings", diff --git a/webshop/webshop/product_data_engine/query.py b/webshop/webshop/product_data_engine/query.py index 4fb6c60543..5a422f5fec 100644 --- a/webshop/webshop/product_data_engine/query.py +++ b/webshop/webshop/product_data_engine/query.py @@ -23,6 +23,7 @@ class ProductQuery: def __init__(self): self.settings = frappe.get_doc("Webshop Settings") self.page_length = self.settings.products_per_page or 20 + self.sort_order = "ranking desc" # New self.or_filters = [] self.filters = [["published", "=", 1]] @@ -43,6 +44,27 @@ def __init__(self): "on_backorder", ] + def set_sort_order(self, sort_by): + sort_map = { + "Last Updated On Asc": "modified asc", + "Last Updated On Desc": "modified desc", + "Website Item Name Asc": "item_name asc", + "Website Item Name Desc": "item_name desc", + "ID Asc": "name asc", + "ID Desc": "name desc", + "Created On Asc": "creation asc", + "Created On Desc": "creation desc", + "Most Used": "ranking desc", + "Route": "route", + "Item Code Asc": "item_code asc", + "Item Code Desc": "item_code desc", + "Item Group Asc": "item_group asc", + "Item Group Desc": "item_group desc", + } + + self.sort_order = sort_map.get(sort_by, "ranking desc") + + def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """ Args: @@ -100,7 +122,8 @@ def query_items(self, start=0): or_filters=self.or_filters, limit_page_length=184467440737095516, limit_start=start, # get all items from this offset for total count ahead - order_by="ranking desc", + order_by=self.sort_order, + # order_by="ranking desc", ) count = len(count_items) @@ -117,7 +140,8 @@ def query_items(self, start=0): or_filters=self.or_filters, limit_page_length=page_length, limit_start=start, - order_by="ranking desc", + order_by=self.sort_order, + # order_by="ranking desc", ) return items, count From 7b2d6a09d41eac05f7356e2b3ccfc0bcf5797e26 Mon Sep 17 00:00:00 2001 From: alaalsalam Date: Thu, 22 May 2025 13:45:59 +0200 Subject: [PATCH 7/9] add style to mobile view with fotter as mobile applicant --- webshop/public/css/custom_style.css | 78 +++++- webshop/public/js/init.js | 101 +++++++- webshop/public/js/product_ui/grid copy.js | 226 ++++++++++++++++++ webshop/public/scss/webshop_cart.scss | 2 +- .../doctype/website_item/website_item.json | 5 +- webshop/webshop/redisearch_utils.py | 1 - 6 files changed, 400 insertions(+), 13 deletions(-) create mode 100644 webshop/public/js/product_ui/grid copy.js diff --git a/webshop/public/css/custom_style.css b/webshop/public/css/custom_style.css index 8422e1b859..7685442348 100644 --- a/webshop/public/css/custom_style.css +++ b/webshop/public/css/custom_style.css @@ -1,10 +1,74 @@ .page-header-wrapper { - display: flex; - justify-content: center; - align-items: center; + display: flex; + justify-content: center; + align-items: center; } -.filters-section{ - background-color: white; - padding: 20px; -} \ No newline at end of file +.filters-section { + background-color: white; + padding: 20px; +} + +/* ========== Mobile Bottom Nav ========== */ +.mobile-bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 9999; + display: flex; + justify-content: space-around; + align-items: center; + background: #ffffff; + border-top: 1px solid #ddd; + box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.05); + padding: 6px 0; + font-size: 12px; +} + +.mobile-bottom-nav .nav-item { + text-align: center; + flex: 1; + color: #444; + text-decoration: none; + position: relative; +} + +.mobile-bottom-nav .nav-item svg.icon, +.mobile-bottom-nav .nav-item .icon-sm, +.mobile-bottom-nav .nav-item .icon-svg { + width: 20px; + height: 20px; + fill: #333; + margin-bottom: 2px; + display: block; +} + +/* ========== Badges ========== */ +.cart-badge, +.shopping-badge { + position: absolute; + top: -4px; + right: 10px; + background-color: #e60023; + color: white; + font-size: 10px; + font-weight: bold; + padding: 2px 6px; + border-radius: 999px; + line-height: 1; + min-width: 18px; + text-align: center; + display: none; +} + +/* ========== Animation on Add ========== */ +.bounce { + animation: bounce 0.4s ease; +} + +@keyframes bounce { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} diff --git a/webshop/public/js/init.js b/webshop/public/js/init.js index fa3ebcaccf..4cf93dd5f3 100644 --- a/webshop/public/js/init.js +++ b/webshop/public/js/init.js @@ -1,2 +1,99 @@ -if (!window.webshop) window.webshop = {} -if (!frappe.boot) frappe.boot = {} +if (!window.webshop) window.webshop = {}; +if (!frappe.boot) frappe.boot = {}; + +frappe.ready(() => { + if (window.innerWidth < 768 && !document.querySelector(".mobile-bottom-nav")) { + + const footerHTML = ` + + `; + document.body.insertAdjacentHTML("beforeend", footerHTML); + + // مزامنة مع العدادات الأصلية + syncNavCountsToFooter(); + + // تحديث بعد الإضافة للسلة + document.body.addEventListener("click", (e) => { + const btn = e.target.closest(".btn-add-to-cart-list"); + if (btn) { + setTimeout(syncNavCountsToFooter, 1000); + } + }); + + // استخدام الكود الأصلي للنظام للإضافة للمفضلة + document.body.addEventListener("click", (e) => { + const icon = e.target.closest(".like-action"); + if (icon && icon.dataset.itemCode) { + e.preventDefault(); + e.stopPropagation(); + // تفعيل الحدث بنفس الكود المستخدم في النظام + icon.click(); + setTimeout(syncNavCountsToFooter, 800); + } + }); + + // تحديث دوري + setInterval(syncNavCountsToFooter, 10000); + } +}); + +function syncNavCountsToFooter() { + // مزامنة العداد مع السلة + const cartTop = document.querySelector("#cart-count"); + const cartMobile = document.querySelector("#cart-count-mobile"); + const cartLi = document.querySelector(".mobile-bottom-nav .cart-icon"); + if (cartTop && cartMobile) { + const val = parseInt(cartTop.innerText || "0"); + cartMobile.innerText = val; + if (val > 0) { + cartMobile.style.display = 'inline-block'; + cartLi && cartLi.classList.remove("hidden"); + } else { + cartMobile.style.display = 'none'; + cartLi && cartLi.classList.add("hidden"); + } + } + + // مزامنة العداد مع المفضلة + const wishTop = document.querySelector("#wish-count"); + const wishMobile = document.querySelector("#wish-count-mobile"); + const wishLi = document.querySelector(".mobile-bottom-nav .wishlist-icon"); + if (wishTop && wishMobile) { + const val = parseInt(wishTop.innerText || "0"); + wishMobile.innerText = val; + if (val > 0) { + wishMobile.style.display = 'inline-block'; + wishLi && wishLi.classList.remove("hidden"); + } else { + wishMobile.style.display = 'none'; + wishLi && wishLi.classList.add("hidden"); + } + } +} diff --git a/webshop/public/js/product_ui/grid copy.js b/webshop/public/js/product_ui/grid copy.js new file mode 100644 index 0000000000..97569a5ebf --- /dev/null +++ b/webshop/public/js/product_ui/grid copy.js @@ -0,0 +1,226 @@ +window.webshop = window.webshop || {}; + +webshop.ProductGrid = class { + /* Options: + - items: Items + - settings: Webshop Settings + - products_section: Products Wrapper + - preference: If preference is not grid view, render but hide + */ + constructor(options) { + Object.assign(this, options); + + if (this.preference !== "Grid View") { + this.products_section.addClass("hidden"); + } + + this.products_section.empty(); + this.make(); + } + + make() { + let me = this; + let html = ``; + + // this.items.forEach(item => { + // let title = item.web_item_name || item.item_name || item.item_code || ""; + // title = title.length > 90 ? title.substr(0, 90) + "..." : title; + + // html += `
`; + // html += me.get_image_html(item, title); + // html += me.get_card_body_html(item, title, me.settings); + // html += `
`; + // }); + + + this.items.forEach(item => { + if (me.settings.hide_unavailable_items && !item.in_stock) { + return; + } + + let title = item.web_item_name || item.item_name || item.item_code || ""; + title = title.length > 90 ? title.substr(0, 90) + "..." : title; + + html += `
`; + html += me.get_image_html(item, title); + html += me.get_card_body_html(item, title, me.settings); + html += `
`; + }); + + let $product_wrapper = this.products_section; + $product_wrapper.append(html); + } + + get_image_html(item, title) { + let image = item.website_image; + + if (image) { + return ` +
+ + ${ title } + +
+ `; + } else { + return ` + + `; + } + } + + get_card_body_html(item, title, settings) { + let body_html = ` +
+
+ `; + body_html += this.get_title(item, title); + + // get floating elements + if (!item.has_variants) { + if (settings.enable_wishlist) { + body_html += this.get_wishlist_icon(item); + } + if (settings.enabled) { + body_html += this.get_cart_indicator(item); + } + + } + + body_html += `
`; + body_html += `
${ item.item_group || '' }
`; + + if (item.formatted_price) { + body_html += this.get_price_html(item); + } + + body_html += this.get_stock_availability(item, settings); + body_html += this.get_primary_button(item, settings); + body_html += `
`; // close div on line 49 + + return body_html; + } + + get_title(item, title) { + let title_html = ` + +
+ ${ title || '' } +
+
+ `; + return title_html; + } + + get_wishlist_icon(item) { + let icon_class = item.wished ? "wished" : "not-wished"; + return ` + + `; + } + + get_cart_indicator(item) { + return ` +
+ 1 +
+ `; + } + + get_price_html(item) { + let price_html = ` +
+ ${ item.formatted_price || '' } + `; + + if (item.formatted_mrp) { + price_html += ` + + ${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" } + + + ${ item.discount } ${ __("OFF") } + + `; + } + price_html += `
`; + return price_html; + } + + get_stock_availability(item, settings) { + if (settings.show_stock_availability && !item.has_variants) { + if (item.on_backorder) { + return ` + + ${ __("Available on backorder") } + + `; + } else if (!item.in_stock) { + return ` + + ${ __("Out of stock") } + + `; + } else { + let qty_display = `${ __("In stock") }`; + if (item.stock_qty) { + qty_display += ` (${item.stock_qty})`; + } + return ` + + ${qty_display} + + `; + } + } + return ``; + } + get_primary_button(item, settings) { + if (item.has_variants) { + return ` + +
+ ${ __("Explore") } +
+
+ `; + } else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) { + return ` +
+ + + + + + ${ settings.enable_checkout ? __("Add to Cart") : __("Add to Quote") } +
+ + +
+ ${ settings.enable_checkout ? __("Go to Cart") : __("Go to Quote") } +
+
+ `; + } else { + return ``; + } + } +}; diff --git a/webshop/public/scss/webshop_cart.scss b/webshop/public/scss/webshop_cart.scss index 46f21eb1c6..e1dd496c4a 100644 --- a/webshop/public/scss/webshop_cart.scss +++ b/webshop/public/scss/webshop_cart.scss @@ -1406,4 +1406,4 @@ body.product-page { .w-fit { width: fit-content !important; -} \ No newline at end of file +} diff --git a/webshop/webshop/doctype/website_item/website_item.json b/webshop/webshop/doctype/website_item/website_item.json index e5adbd778c..4c55e05177 100644 --- a/webshop/webshop/doctype/website_item/website_item.json +++ b/webshop/webshop/doctype/website_item/website_item.json @@ -184,7 +184,9 @@ "fetch_from": "item_code.item_group", "fieldname": "item_group", "fieldtype": "Link", + "in_filter": 1, "in_list_view": 1, + "in_standard_filter": 1, "label": "Item Group", "options": "Item Group", "read_only": 1, @@ -202,7 +204,6 @@ "fetch_from": "item_code.has_variants", "fieldname": "has_variants", "fieldtype": "Check", - "in_standard_filter": 1, "label": "Has Variants", "no_copy": 1, "read_only": 1 @@ -347,7 +348,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2024-11-05 13:41:45.347700", + "modified": "2025-05-19 14:39:31.456099", "modified_by": "Administrator", "module": "Webshop", "name": "Website Item", diff --git a/webshop/webshop/redisearch_utils.py b/webshop/webshop/redisearch_utils.py index f52137f19f..57514fb5a5 100644 --- a/webshop/webshop/redisearch_utils.py +++ b/webshop/webshop/redisearch_utils.py @@ -158,7 +158,6 @@ def delete_item_from_index(website_item_doc): def delete_from_ac_dict(website_item_doc): """Removes this items's name from autocomplete dictionary""" ac = frappe.cache().ft() - # ac.sugdel(website_item_doc.web_item_name) ac.sugdel("autocomplete_key", website_item_doc.web_item_name) From 440da2e20312fb89601232755d0e36684f704181 Mon Sep 17 00:00:00 2001 From: alaalsalam Date: Thu, 22 May 2025 16:24:50 +0200 Subject: [PATCH 8/9] add confirm before request a quotation --- webshop/public/css/custom_style.css | 7 +++ webshop/public/js/init.js | 63 +++++++------------ .../templates/includes/cart/place_order.html | 2 +- webshop/templates/pages/cart.js | 27 ++++++-- 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/webshop/public/css/custom_style.css b/webshop/public/css/custom_style.css index 7685442348..f1769340f0 100644 --- a/webshop/public/css/custom_style.css +++ b/webshop/public/css/custom_style.css @@ -72,3 +72,10 @@ 50% { transform: scale(1.2); } 100% { transform: scale(1); } } +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} \ No newline at end of file diff --git a/webshop/public/js/init.js b/webshop/public/js/init.js index 4cf93dd5f3..c2a4324883 100644 --- a/webshop/public/js/init.js +++ b/webshop/public/js/init.js @@ -3,7 +3,6 @@ if (!frappe.boot) frappe.boot = {}; frappe.ready(() => { if (window.innerWidth < 768 && !document.querySelector(".mobile-bottom-nav")) { - const footerHTML = ` `; document.body.insertAdjacentHTML("beforeend", footerHTML); + } - // مزامنة مع العدادات الأصلية - syncNavCountsToFooter(); - - // تحديث بعد الإضافة للسلة - document.body.addEventListener("click", (e) => { - const btn = e.target.closest(".btn-add-to-cart-list"); - if (btn) { - setTimeout(syncNavCountsToFooter, 1000); - } - }); + document.body.addEventListener("click", (e) => { + const btn = e.target.closest(".btn-add-to-cart-list"); + if (btn) { + setTimeout(syncNavCountsToFooter, 1000); + } + }); - // استخدام الكود الأصلي للنظام للإضافة للمفضلة - document.body.addEventListener("click", (e) => { - const icon = e.target.closest(".like-action"); - if (icon && icon.dataset.itemCode) { - e.preventDefault(); - e.stopPropagation(); - // تفعيل الحدث بنفس الكود المستخدم في النظام - icon.click(); - setTimeout(syncNavCountsToFooter, 800); - } - }); + document.body.addEventListener("click", (e) => { + const icon = e.target.closest(".like-action"); + if (icon && icon.dataset.itemCode) { + e.preventDefault(); + e.stopPropagation(); + icon.click(); + setTimeout(syncNavCountsToFooter, 800); + } + }); - // تحديث دوري - setInterval(syncNavCountsToFooter, 10000); - } + setInterval(syncNavCountsToFooter, 10000); + syncNavCountsToFooter(); }); function syncNavCountsToFooter() { - // مزامنة العداد مع السلة const cartTop = document.querySelector("#cart-count"); const cartMobile = document.querySelector("#cart-count-mobile"); const cartLi = document.querySelector(".mobile-bottom-nav .cart-icon"); if (cartTop && cartMobile) { const val = parseInt(cartTop.innerText || "0"); cartMobile.innerText = val; - if (val > 0) { - cartMobile.style.display = 'inline-block'; - cartLi && cartLi.classList.remove("hidden"); - } else { - cartMobile.style.display = 'none'; - cartLi && cartLi.classList.add("hidden"); - } + cartLi?.classList.toggle("hidden", val === 0); + cartMobile.style.display = val > 0 ? "inline-block" : "none"; } - // مزامنة العداد مع المفضلة const wishTop = document.querySelector("#wish-count"); const wishMobile = document.querySelector("#wish-count-mobile"); const wishLi = document.querySelector(".mobile-bottom-nav .wishlist-icon"); if (wishTop && wishMobile) { const val = parseInt(wishTop.innerText || "0"); wishMobile.innerText = val; - if (val > 0) { - wishMobile.style.display = 'inline-block'; - wishLi && wishLi.classList.remove("hidden"); - } else { - wishMobile.style.display = 'none'; - wishLi && wishLi.classList.add("hidden"); - } + wishLi?.classList.toggle("hidden", val === 0); + wishMobile.style.display = val > 0 ? "inline-block" : "none"; } } diff --git a/webshop/templates/includes/cart/place_order.html b/webshop/templates/includes/cart/place_order.html index a6d638bfea..162b31b338 100644 --- a/webshop/templates/includes/cart/place_order.html +++ b/webshop/templates/includes/cart/place_order.html @@ -10,4 +10,4 @@ {% endif %} - \ No newline at end of file + diff --git a/webshop/templates/pages/cart.js b/webshop/templates/pages/cart.js index dd03f3d1c7..f7c328f92a 100644 --- a/webshop/templates/pages/cart.js +++ b/webshop/templates/pages/cart.js @@ -21,18 +21,33 @@ $.extend(shopping_cart, { shopping_cart.bind_remove_coupon_code(); }, - bind_place_order: function() { - $(".btn-place-order").on("click", function() { - shopping_cart.place_order(this); + bind_place_order: function () { + $(".btn-place-order").off("click").on("click", function (e) { + e.preventDefault(); + const btn = this; + frappe.confirm( + "Are you sure you want to place this order?", + function () { + shopping_cart.place_order(btn); + } + ); }); }, - bind_request_quotation: function() { - $('.btn-request-for-quotation').on('click', function() { - shopping_cart.request_quotation(this); + bind_request_quotation: function () { + $(".btn-request-for-quotation").off("click").on("click", function (e) { + e.preventDefault(); + const btn = this; + frappe.confirm( + "Are you sure you want to request a quotation?", + function () { + shopping_cart.request_quotation(btn); + } + ); }); }, + bind_change_qty: function() { // bind update button $(".cart-items").on("change", ".cart-qty", function() { From a96877780349e26f93a064dbd50c47d42d1c7c23 Mon Sep 17 00:00:00 2001 From: MohamedAbdulsalam96 Date: Sun, 1 Jun 2025 11:20:15 +0200 Subject: [PATCH 9/9] add filter to phone view and add new style for phone view --- webshop/public/css/custom_style.css | 47 ++++++++++++++++- webshop/public/js/init.js | 26 +++++++--- webshop/public/js/product_ui/views.js | 8 +-- webshop/www/all-products/index copy.html | 51 ++++++++++++++++++ webshop/www/all-products/index.html | 66 ++++++++++++++++++++++-- 5 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 webshop/www/all-products/index copy.html diff --git a/webshop/public/css/custom_style.css b/webshop/public/css/custom_style.css index f1769340f0..51103e99c6 100644 --- a/webshop/public/css/custom_style.css +++ b/webshop/public/css/custom_style.css @@ -10,6 +10,12 @@ } /* ========== Mobile Bottom Nav ========== */ + +@media (min-width: 992px) { + .mobile-bottom-nav { + display: none !important; + } +} .mobile-bottom-nav { position: fixed; bottom: 0; @@ -48,8 +54,7 @@ .cart-badge, .shopping-badge { position: absolute; - top: -4px; - right: 10px; + top: 0; background-color: #e60023; color: white; font-size: 10px; @@ -60,6 +65,9 @@ min-width: 18px; text-align: center; display: none; + width: fit-content; + left: 50%; + transform: translate(-30px, 0); } /* ========== Animation on Add ========== */ @@ -78,4 +86,39 @@ left: 0; right: 0; z-index: 1000; +} + + +.product-paging-area { + margin-bottom: 60px; +} + +.breadcrumb-container { + margin-top: 2.5rem; +} + +#filters-btn { + display: block; + position: fixed; + bottom: 80px; + left: 11px; + z-index: 100; + background-color: #0289f7; + border-radius: 50%; + padding: 7px; +} + +#filters-btn svg { + stroke: #ffffff; + height: 29px; + width: 20px; +} + +.btn-add-to-cart-list { + text-wrap: auto; + height: auto; + max-height: unset; + width: fit-content; + min-width: 100px !important; + padding: 0.25rem 0.2rem !important; } \ No newline at end of file diff --git a/webshop/public/js/init.js b/webshop/public/js/init.js index c2a4324883..eb74c641ae 100644 --- a/webshop/public/js/init.js +++ b/webshop/public/js/init.js @@ -2,15 +2,20 @@ if (!window.webshop) window.webshop = {}; if (!frappe.boot) frappe.boot = {}; frappe.ready(() => { - if (window.innerWidth < 768 && !document.querySelector(".mobile-bottom-nav")) { + // if (window.innerWidth < 768 && !document.querySelector(".mobile-bottom-nav")) { + if ( !document.querySelector(".mobile-bottom-nav")) { const footerHTML = ` @@ -39,7 +49,7 @@ frappe.ready(() => { document.body.addEventListener("click", (e) => { const btn = e.target.closest(".btn-add-to-cart-list"); if (btn) { - setTimeout(syncNavCountsToFooter, 1000); + setTimeout(syncNavCountsToFooter, 200); } }); @@ -49,12 +59,12 @@ frappe.ready(() => { e.preventDefault(); e.stopPropagation(); icon.click(); - setTimeout(syncNavCountsToFooter, 800); + setTimeout(syncNavCountsToFooter, 200); } }); - setInterval(syncNavCountsToFooter, 10000); syncNavCountsToFooter(); + setInterval(syncNavCountsToFooter, 2000); }); function syncNavCountsToFooter() { diff --git a/webshop/public/js/product_ui/views.js b/webshop/public/js/product_ui/views.js index e65f45437a..e8a19bbaab 100644 --- a/webshop/public/js/product_ui/views.js +++ b/webshop/public/js/product_ui/views.js @@ -152,9 +152,8 @@ webshop.ProductView = class { if (this.products) { let paging_html = `
-
-
-
+
+
`; let query_params = frappe.utils.get_query_params(); let start = query_params.start ? cint(JSON.parse(query_params.start)) : 0; @@ -176,7 +175,8 @@ webshop.ProductView = class { `; - paging_html += `
`; + paging_html += `
+
`; $(".page_content").append(paging_html); this.bind_paging_action(); diff --git a/webshop/www/all-products/index copy.html b/webshop/www/all-products/index copy.html new file mode 100644 index 0000000000..cbd76632b6 --- /dev/null +++ b/webshop/www/all-products/index copy.html @@ -0,0 +1,51 @@ +{% from "webshop/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %} +{% extends "templates/web.html" %} + +{% block title %}{{ _("All Products") }}{% endblock %} +{% block header %} +
{{ _("All Products") }}
+{% endblock header %} + +{% block page_content %} +
+ +
+ +
+ + +
+
+
+
{{ _('Filters') }}
+ {{ _('Clear All') }} +
+ + {% if field_filters %} + {{ _(field_filter_section(field_filters)) }} + {% endif %} + + + {% if attribute_filters %} + {{ _(attribute_filter_section(attribute_filters)) }} + {% endif %} +
+ +
+
+ + + +{% endblock %} diff --git a/webshop/www/all-products/index.html b/webshop/www/all-products/index.html index cbd76632b6..4678e01fe8 100644 --- a/webshop/www/all-products/index.html +++ b/webshop/www/all-products/index.html @@ -8,18 +8,28 @@ {% block page_content %}
+ +
+ +
+
- -
-
+ +
+
-
{{ _('Filters') }}
+
{{ _('Filters') }}
{{ _('Clear All') }}
+ {% if field_filters %} {{ _(field_filter_section(field_filters)) }} @@ -30,10 +40,54 @@ {{ _(attribute_filter_section(attribute_filters)) }} {% endif %}
-
+ + + + + + + + + {% endblock %}