Features: 1) Add top 10 most wished and most popular products lists; 2) Update UI to display lists with cards and counts.

Fixes: 1) Fix translation of "Returns" to "Возвраты"; 2) Fix translation of "Sales vs Returns" to "Продажи и Возвраты".

Extra: 1) Refactor wishlist and order product queries to support top 10 lists; 2) Add backward compatibility for single product views; 3) Update template structure to include new lists and improve layout.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-11-16 17:02:36 +03:00
parent d4ea32c375
commit d2f46539ee
3 changed files with 101 additions and 18 deletions

View file

@ -2805,7 +2805,7 @@ msgstr "Выручка (нетто, 30d)"
#: engine/core/templates/admin/index.html:43 #: engine/core/templates/admin/index.html:43
msgid "Returns (30d)" msgid "Returns (30d)"
msgstr "Возвращение (30 дней)" msgstr "Возвраты (30 дней)"
#: engine/core/templates/admin/index.html:52 #: engine/core/templates/admin/index.html:52
msgid "Processed orders (30d)" msgid "Processed orders (30d)"
@ -2813,7 +2813,7 @@ msgstr "Обработанные заказы (30d)"
#: engine/core/templates/admin/index.html:65 #: engine/core/templates/admin/index.html:65
msgid "Sales vs Returns (30d)" msgid "Sales vs Returns (30d)"
msgstr "Продажи против возвратов (30d)" msgstr "Продажи и Возвраты (30d)"
#: engine/core/templates/admin/index.html:82 #: engine/core/templates/admin/index.html:82
msgid "Gross" msgid "Gross"

View file

@ -15,6 +15,7 @@
{% block content %} {% block content %}
{% component "unfold/components/container.html" %} {% component "unfold/components/container.html" %}
<div class="flex flex-col min-h-screen">
{% component "unfold/components/title.html" %} {% component "unfold/components/title.html" %}
{% trans "Dashboard" %} {% trans "Dashboard" %}
<br/> <br/>
@ -121,10 +122,26 @@
{% component "unfold/components/title.html" %} {% component "unfold/components/title.html" %}
{% trans "Most wished product" %} {% trans "Most wished product" %}
{% endcomponent %} {% endcomponent %}
{% if most_wished_product %} {% if most_wished_products %}
<ul class="flex flex-col divide-y divide-gray-200 dark:divide-base-700/50">
{% for p in most_wished_products %}
<li class="py-2 first:pt-0 last:pb-0">
<a href="{{ p.admin_url }}" class="flex items-center gap-4">
{% if p.image %}
<img src="{{ p.image }}" alt="{{ p.name }}" class="w-12 h-12 object-cover rounded"/>
{% endif %}
<span class="font-medium flex-1 truncate">{{ p.name }}</span>
<span class="text-xs px-2 py-0.5 rounded bg-base-700/[.06] dark:bg-white/[.06] text-gray-700 dark:text-gray-200">{{ p.count }}</span>
</a>
</li>
{% endfor %}
</ul>
{% elif most_wished_product %}
<a href="{{ most_wished_product.admin_url }}" class="flex items-center gap-4"> <a href="{{ most_wished_product.admin_url }}" class="flex items-center gap-4">
<img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}" {% if most_wished_product.image %}
class="w-16 h-16 object-cover rounded"/> <img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}"
class="w-16 h-16 object-cover rounded"/>
{% endif %}
<span class="font-medium">{{ most_wished_product.name }}</span> <span class="font-medium">{{ most_wished_product.name }}</span>
</a> </a>
{% else %} {% else %}
@ -138,10 +155,26 @@
{% component "unfold/components/title.html" %} {% component "unfold/components/title.html" %}
{% trans "Most popular product" %} {% trans "Most popular product" %}
{% endcomponent %} {% endcomponent %}
{% if most_popular_product %} {% if most_popular_products %}
<ul class="flex flex-col divide-y divide-gray-200 dark:divide-base-700/50">
{% for p in most_popular_products %}
<li class="py-2 first:pt-0 last:pb-0">
<a href="{{ p.admin_url }}" class="flex items-center gap-4">
{% if p.image %}
<img src="{{ p.image }}" alt="{{ p.name }}" class="w-12 h-12 object-cover rounded"/>
{% endif %}
<span class="font-medium flex-1 truncate">{{ p.name }}</span>
<span class="text-xs px-2 py-0.5 rounded bg-base-700/[.06] dark:bg-white/[.06] text-gray-700 dark:text-gray-200">{{ p.count }}</span>
</a>
</li>
{% endfor %}
</ul>
{% elif most_popular_product %}
<a href="{{ most_popular_product.admin_url }}" class="flex items-center gap-4"> <a href="{{ most_popular_product.admin_url }}" class="flex items-center gap-4">
<img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}" {% if most_popular_product.image %}
class="w-16 h-16 object-cover rounded"/> <img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}"
class="w-16 h-16 object-cover rounded"/>
{% endif %}
<span class="font-medium">{{ most_popular_product.name }}</span> <span class="font-medium">{{ most_popular_product.name }}</span>
</a> </a>
{% else %} {% else %}
@ -156,12 +189,12 @@
{% component "unfold/components/separator.html" %} {% component "unfold/components/separator.html" %}
{% endcomponent %} {% endcomponent %}
<div class="mt-4"> <div class="mt-4 mt-auto">
<br/>
{% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %} {% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %}
eVibes {{ evibes_version }} · Wiseless Team eVibes {{ evibes_version }} · Wiseless Team
{% endcomponent %} {% endcomponent %}
</div> </div>
</div>
{% endcomponent %} {% endcomponent %}
{% endblock %} {% endblock %}

View file

@ -438,18 +438,21 @@ def dashboard_callback(request, context):
} }
) )
# Single most wished product (backward compatibility)
most_wished: dict[str, str | int | float | None] | None = None most_wished: dict[str, str | int | float | None] | None = None
# Top 10 most wished products
most_wished_list: list[dict[str, str | int | float | None]] = []
with suppress(Exception): with suppress(Exception):
wished = ( wished_qs = (
Wishlist.objects.filter(user__is_active=True, user__is_staff=False) Wishlist.objects.filter(user__is_active=True, user__is_staff=False)
.values("products") .values("products")
.exclude(products__isnull=True) .exclude(products__isnull=True)
.annotate(cnt=Count("products")) .annotate(cnt=Count("products"))
.order_by("-cnt") .order_by("-cnt")
.first()
) )
if wished and wished.get("products"): wished_first = wished_qs.first()
product = Product.objects.filter(pk=wished["products"]).first() if wished_first and wished_first.get("products"):
product = Product.objects.filter(pk=wished_first["products"]).first()
if product: if product:
img = product.images.first().image_url if product.images.exists() else "" img = product.images.first().image_url if product.images.exists() else ""
most_wished = { most_wished = {
@ -458,17 +461,42 @@ def dashboard_callback(request, context):
"admin_url": reverse("admin:core_product_change", args=[product.pk]), "admin_url": reverse("admin:core_product_change", args=[product.pk]),
} }
# Build top 10 list
wished_top10 = list(wished_qs[:10])
if wished_top10:
counts_map = {row["products"]: row["cnt"] for row in wished_top10 if row.get("products")}
products = Product.objects.filter(pk__in=counts_map.keys())
# Preserve order as in wished_top10
product_by_id = {p.pk: p for p in products}
for row in wished_top10:
pid = row.get("products")
if not pid or pid not in product_by_id:
continue
p = product_by_id[pid]
img = p.images.first().image_url if p.images.exists() else ""
most_wished_list.append(
{
"name": p.name,
"image": img,
"admin_url": reverse("admin:core_product_change", args=[p.pk]),
"count": int(row.get("cnt", 0)),
}
)
# Single most popular product (backward compatibility)
most_popular: dict[str, str | int | float | None] | None = None most_popular: dict[str, str | int | float | None] | None = None
# Top 10 most popular products by quantity
most_popular_list: list[dict[str, str | int | float | None]] = []
with suppress(Exception): with suppress(Exception):
popular = ( popular_qs = (
OrderProduct.objects.filter(status="FINISHED", order__status="FINISHED", product__isnull=False) OrderProduct.objects.filter(status="FINISHED", order__status="FINISHED", product__isnull=False)
.values("product") .values("product")
.annotate(total_qty=Sum("quantity")) .annotate(total_qty=Sum("quantity"))
.order_by("-total_qty") .order_by("-total_qty")
.first()
) )
if popular and popular.get("product"): popular_first = popular_qs.first()
product = Product.objects.filter(pk=popular["product"]).first() if popular_first and popular_first.get("product"):
product = Product.objects.filter(pk=popular_first["product"]).first()
if product: if product:
img = product.images.first().image_url if product.images.exists() else "" img = product.images.first().image_url if product.images.exists() else ""
most_popular = { most_popular = {
@ -477,6 +505,26 @@ def dashboard_callback(request, context):
"admin_url": reverse("admin:core_product_change", args=[product.pk]), "admin_url": reverse("admin:core_product_change", args=[product.pk]),
} }
popular_top10 = list(popular_qs[:10])
if popular_top10:
qty_map = {row["product"]: row["total_qty"] for row in popular_top10 if row.get("product")}
products = Product.objects.filter(pk__in=qty_map.keys())
product_by_id = {p.pk: p for p in products}
for row in popular_top10:
pid = row.get("product")
if not pid or pid not in product_by_id:
continue
p = product_by_id[pid]
img = p.images.first().image_url if p.images.exists() else ""
most_popular_list.append(
{
"name": p.name,
"image": img,
"admin_url": reverse("admin:core_product_change", args=[p.pk]),
"count": int(row.get("total_qty", 0) or 0),
}
)
context.update( context.update(
{ {
"custom_variable": "value", "custom_variable": "value",
@ -488,6 +536,8 @@ def dashboard_callback(request, context):
"quick_links": quick_links, "quick_links": quick_links,
"most_wished_product": most_wished, "most_wished_product": most_wished,
"most_popular_product": most_popular, "most_popular_product": most_popular,
"most_wished_products": most_wished_list,
"most_popular_products": most_popular_list,
} }
) )