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:
parent
d4ea32c375
commit
d2f46539ee
3 changed files with 101 additions and 18 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
{% if most_wished_product.image %}
|
||||||
<img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}"
|
<img src="{{ most_wished_product.image }}" alt="{{ most_wished_product.name }}"
|
||||||
class="w-16 h-16 object-cover rounded"/>
|
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">
|
||||||
|
{% if most_popular_product.image %}
|
||||||
<img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}"
|
<img src="{{ most_popular_product.image }}" alt="{{ most_popular_product.name }}"
|
||||||
class="w-16 h-16 object-cover rounded"/>
|
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 %}
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue