diff --git a/engine/core/templates/admin/dashboard/_customers_mix.html b/engine/core/templates/admin/dashboard/_customers_mix.html
new file mode 100644
index 00000000..74a5af15
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_customers_mix.html
@@ -0,0 +1,28 @@
+{% load i18n unfold %}
+
+{% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Customers mix (30d)" %}
+ {% endcomponent %}
+ {% if customers_mix.total|default:0 > 0 %}
+
+
+ {% trans "New customers" %}
+ {{ customers_mix.new }} ({{ customers_mix.new_pct }}%)
+
+ {% component "unfold/components/progress.html" with value=customers_mix.new_pct title='' description='' %}
+ {% endcomponent %}
+
+
+ {% trans "Returning customers" %}
+ {{ customers_mix.returning }} ({{ customers_mix.returning_pct }}%)
+
+ {% component "unfold/components/progress.html" with value=customers_mix.returning_pct title='' description='' %}
+ {% endcomponent %}
+
+ {% else %}
+ {% component "unfold/components/text.html" %}
+ {% trans "No customer activity in the last 30 days." %}
+ {% endcomponent %}
+ {% endif %}
+{% endcomponent %}
diff --git a/engine/core/templates/admin/dashboard/_daily_sales.html b/engine/core/templates/admin/dashboard/_daily_sales.html
new file mode 100644
index 00000000..58c30b0f
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_daily_sales.html
@@ -0,0 +1,129 @@
+{% load i18n unfold static %}
+
+{% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Daily sales (30d)" %}
+ {% endcomponent %}
+
+
+
+
+ {{ daily_labels|json_script:"daily-labels" }}
+ {{ daily_orders|json_script:"daily-orders" }}
+ {{ daily_gross|json_script:"daily-gross" }}
+
+{% endcomponent %}
diff --git a/engine/core/templates/admin/dashboard/_income_overview.html b/engine/core/templates/admin/dashboard/_income_overview.html
new file mode 100644
index 00000000..c42c8c29
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_income_overview.html
@@ -0,0 +1,118 @@
+{% load i18n unfold arith %}
+
+{% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 %}
+ {% with total=gross|add:returns %}
+ {% component "unfold/components/card.html" with class="xl:col-span-2" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Income overview" %}
+ {% endcomponent %}
+
+ {% with net=revenue_net_30|default:0 %}
+ {% with tax_amt=gross|sub:net %}
+ {% with returns_capped=returns %}
+ {% if returns > gross %}
+ {% with returns_capped=gross %}{% endwith %}
+ {% endif %}
+ {% with tax_amt_pos=tax_amt %}
+ {% if tax_amt_pos < 0 %}
+ {% with tax_amt_pos=0 %}{% endwith %}
+ {% endif %}
+ {% with net_for_pie=gross|sub:tax_amt_pos|sub:returns_capped %}
+ {% if net_for_pie < 0 %}
+ {% with net_for_pie=0 %}{% endwith %}
+ {% endif %}
+
+
+
+
+
+
+
+ {% trans "Net" %}:
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ net }}
+
+ {% if tax_amt_pos > 0 %}
+
+
+ {% trans "Taxes" %}:
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ tax_amt_pos|floatformat:2 }}
+
+ {% endif %}
+
+
+ {% trans "Returns" %}:
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns }}
+
+
+
+ {% trans "Gross (pie total)" %}:
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ gross }}
+
+
+
+
+
+ {% endwith %}
+ {% endwith %}
+ {% endwith %}
+ {% endwith %}
+ {% endwith %}
+ {% endcomponent %}
+ {% endwith %}
+{% endwith %}
diff --git a/engine/core/templates/admin/dashboard/_kpis.html b/engine/core/templates/admin/dashboard/_kpis.html
new file mode 100644
index 00000000..85483682
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_kpis.html
@@ -0,0 +1,39 @@
+{% load i18n unfold %}
+
+
+ {% component "unfold/components/card.html" %}
+ {% component "unfold/components/text.html" %}
+ {% trans "Revenue (gross, 30d)" %}
+ {% endcomponent %}
+ {% component "unfold/components/title.html" %}
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_gross_30|default:0 }}
+ {% endcomponent %}
+ {% endcomponent %}
+
+ {% component "unfold/components/card.html" %}
+ {% component "unfold/components/text.html" %}
+ {% trans "Revenue (net, 30d)" %}
+ {% endcomponent %}
+ {% component "unfold/components/title.html" %}
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_net_30|default:0 }}
+ {% endcomponent %}
+ {% endcomponent %}
+
+ {% component "unfold/components/card.html" %}
+ {% component "unfold/components/text.html" %}
+ {% trans "Returns (30d)" %}
+ {% endcomponent %}
+ {% component "unfold/components/title.html" %}
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns_30|default:0 }}
+ {% endcomponent %}
+ {% endcomponent %}
+
+ {% component "unfold/components/card.html" %}
+ {% component "unfold/components/text.html" %}
+ {% trans "Processed orders (30d)" %}
+ {% endcomponent %}
+ {% component "unfold/components/title.html" %}
+ {{ processed_orders_30|default:0 }}
+ {% endcomponent %}
+ {% endcomponent %}
+
diff --git a/engine/core/templates/admin/dashboard/_most_returned.html b/engine/core/templates/admin/dashboard/_most_returned.html
new file mode 100644
index 00000000..d9514d5d
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_most_returned.html
@@ -0,0 +1,27 @@
+{% load i18n unfold %}
+
+{% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Most returned products (30d)" %}
+ {% endcomponent %}
+ {% if most_returned_products %}
+
+ {% else %}
+ {% component "unfold/components/text.html" %}
+ {% trans "No returns in the last 30 days." %}
+ {% endcomponent %}
+ {% endif %}
+{% endcomponent %}
diff --git a/engine/core/templates/admin/dashboard/_product_lists.html b/engine/core/templates/admin/dashboard/_product_lists.html
new file mode 100644
index 00000000..a71b0095
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_product_lists.html
@@ -0,0 +1,71 @@
+{% load i18n unfold %}
+
+
+ {% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Most wished product" %}
+ {% endcomponent %}
+ {% if most_wished_products %}
+
+ {% elif most_wished_product %}
+
+ {% if most_wished_product.image %}
+
+ {% endif %}
+ {{ most_wished_product.name }}
+
+ {% else %}
+ {% component "unfold/components/text.html" %}
+ {% trans "No data yet." %}
+ {% endcomponent %}
+ {% endif %}
+ {% endcomponent %}
+
+ {% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Most popular product" %}
+ {% endcomponent %}
+ {% if most_popular_products %}
+
+ {% elif most_popular_product %}
+
+ {% if most_popular_product.image %}
+
+ {% endif %}
+ {{ most_popular_product.name }}
+
+ {% else %}
+ {% component "unfold/components/text.html" %}
+ {% trans "No data yet." %}
+ {% endcomponent %}
+ {% endif %}
+ {% endcomponent %}
+
diff --git a/engine/core/templates/admin/dashboard/_quick_links.html b/engine/core/templates/admin/dashboard/_quick_links.html
new file mode 100644
index 00000000..8ed55eb4
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_quick_links.html
@@ -0,0 +1,15 @@
+{% load i18n unfold %}
+
+{% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Quick Links" %}
+ {% endcomponent %}
+ {% if quick_links %}
+ {% component "unfold/components/navigation.html" with class="flex flex-col gap-1" items=quick_links %}
+ {% endcomponent %}
+ {% else %}
+ {% component "unfold/components/text.html" %}
+ {% trans "No links available." %}
+ {% endcomponent %}
+ {% endif %}
+{% endcomponent %}
diff --git a/engine/core/templates/admin/dashboard/_shipped_vs_digital.html b/engine/core/templates/admin/dashboard/_shipped_vs_digital.html
new file mode 100644
index 00000000..6998b32c
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_shipped_vs_digital.html
@@ -0,0 +1,29 @@
+{% load i18n unfold %}
+
+{% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Shipped vs Digital (30d)" %}
+ {% endcomponent %}
+
+
+
+ {% trans "Digital" %}
+
+ {{ shipped_vs_digital.digital_qty }} ({% firstof shipped_vs_digital.digital_pct 0 %}%) ·
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ shipped_vs_digital.digital_gross }}
+
+
+ {% component "unfold/components/progress.html" with value=shipped_vs_digital.digital_pct title='' description='' %}
+ {% endcomponent %}
+
+
+ {% trans "Shipped" %}
+
+ {{ shipped_vs_digital.shipped_qty }} ({% firstof shipped_vs_digital.shipped_pct 0 %}%) ·
+ {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ shipped_vs_digital.shipped_gross }}
+
+
+ {% component "unfold/components/progress.html" with value=shipped_vs_digital.shipped_pct title='' description='' %}
+ {% endcomponent %}
+
+{% endcomponent %}
diff --git a/engine/core/templates/admin/dashboard/_top_categories.html b/engine/core/templates/admin/dashboard/_top_categories.html
new file mode 100644
index 00000000..fe51b251
--- /dev/null
+++ b/engine/core/templates/admin/dashboard/_top_categories.html
@@ -0,0 +1,26 @@
+{% load i18n unfold %}
+
+{% component "unfold/components/card.html" %}
+ {% component "unfold/components/title.html" %}
+ {% trans "Top categories by quantity (30d)" %}
+ {% endcomponent %}
+ {% if top_categories %}
+
+ {% else %}
+ {% component "unfold/components/text.html" %}
+ {% trans "No category sales in the last 30 days." %}
+ {% endcomponent %}
+ {% endif %}
+{% endcomponent %}
diff --git a/engine/core/templates/admin/index.html b/engine/core/templates/admin/index.html
index 45b74158..92693e81 100644
--- a/engine/core/templates/admin/index.html
+++ b/engine/core/templates/admin/index.html
@@ -21,383 +21,28 @@
{% endcomponent %}
-
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/text.html" %}
- {% trans "Revenue (gross, 30d)" %}
- {% endcomponent %}
- {% component "unfold/components/title.html" %}
- {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_gross_30|default:0 }}
- {% endcomponent %}
- {% endcomponent %}
-
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/text.html" %}
- {% trans "Revenue (net, 30d)" %}
- {% endcomponent %}
- {% component "unfold/components/title.html" %}
- {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_net_30|default:0 }}
- {% endcomponent %}
- {% endcomponent %}
-
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/text.html" %}
- {% trans "Returns (30d)" %}
- {% endcomponent %}
- {% component "unfold/components/title.html" %}
- {% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns_30|default:0 }}
- {% endcomponent %}
- {% endcomponent %}
-
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/text.html" %}
- {% trans "Processed orders (30d)" %}
- {% endcomponent %}
- {% component "unfold/components/title.html" %}
- {{ processed_orders_30|default:0 }}
- {% endcomponent %}
- {% endcomponent %}
-
+ {% include "admin/dashboard/_kpis.html" %}
- {% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 %}
- {% with total=gross|add:returns %}
- {% component "unfold/components/card.html" with class="xl:col-span-2" %}
- {% component "unfold/components/title.html" %}
- {% trans "Income overview" %}
- {% endcomponent %}
- {% if total and total > 0 %}
- {% with net=revenue_net_30|default:0 %}
- {% with tax_amt=gross|sub:net %}
- {% with returns_capped=returns %}
- {% if returns > gross %}
- {% with returns_capped=gross %}{% endwith %}
- {% endif %}
- {% with tax_amt_pos=tax_amt %}
- {% if tax_amt_pos < 0 %}
- {% with tax_amt_pos=0 %}{% endwith %}
- {% endif %}
- {% with net_for_pie=gross|sub:tax_amt_pos|sub:returns_capped %}
- {% if net_for_pie < 0 %}
- {% with net_for_pie=0 %}{% endwith %}
- {% endif %}
- {% widthratio net_for_pie gross 360 as net_deg %}
- {% widthratio tax_amt_pos gross 360 as tax_deg %}
- {% widthratio returns_capped gross 360 as ret_deg %}
- {% with net_end=net_deg %}
- {% with tax_end=net_end|add:tax_deg %}
- {% with ret_end=tax_end|add:ret_deg %}
-
-
-
-
-
-
-
- {% trans "Net" %}:
- {% if currency_symbol %}
- {{ currency_symbol }}{% endif %}{{ net }}
-
- {% if tax_amt_pos > 0 %}
-
-
- {% trans "Taxes" %}:
- {% if currency_symbol %}
- {{ currency_symbol }}{% endif %}{{ tax_amt_pos|floatformat:2 }}
-
- {% endif %}
-
-
- {% trans "Returns" %}:
- {% if currency_symbol %}
- {{ currency_symbol }}{% endif %}{{ returns }}
-
-
-
- {% trans "Gross (pie total)" %}:
- {% if currency_symbol %}
- {{ currency_symbol }}{% endif %}{{ gross }}
-
-
-
-
-
- {% endwith %}
- {% endwith %}
- {% endwith %}
- {% endwith %}
- {% endwith %}
- {% endwith %}
- {% endwith %}
- {% endwith %}
- {% else %}
- {% component "unfold/components/text.html" %}
- {% trans "Not enough data for chart yet." %}
- {% endcomponent %}
- {% endif %}
- {% endcomponent %}
- {% endwith %}
- {% endwith %}
-
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/title.html" %}
- {% trans "Quick Links" %}
- {% endcomponent %}
- {% if quick_links %}
- {% component "unfold/components/navigation.html" with class="flex flex-col gap-1" items=quick_links %}
- {% endcomponent %}
- {% else %}
- {% component "unfold/components/text.html" %}
- {% trans "No links available." %}
- {% endcomponent %}
- {% endif %}
- {% endcomponent %}
+ {% include "admin/dashboard/_income_overview.html" %}
+ {% include "admin/dashboard/_quick_links.html" %}
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/title.html" %}
- {% trans "Daily sales (30d)" %}
- {% endcomponent %}
- {% if daily_labels and daily_labels|length > 0 %}
-
-
-
-
- {{ daily_labels|json_script:"daily-labels" }}
- {{ daily_orders|json_script:"daily-orders" }}
- {{ daily_gross|json_script:"daily-gross" }}
-
- {% else %}
- {% component "unfold/components/text.html" %}
- {% trans "Not enough data for chart yet." %}
- {% endcomponent %}
- {% endif %}
- {% endcomponent %}
+ {% include "admin/dashboard/_daily_sales.html" %}
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/title.html" %}
- {% trans "Most wished product" %}
- {% endcomponent %}
- {% if most_wished_products %}
-
- {% elif most_wished_product %}
-
- {% if most_wished_product.image %}
-
- {% endif %}
- {{ most_wished_product.name }}
-
- {% else %}
- {% component "unfold/components/text.html" %}
- {% trans "No data yet." %}
- {% endcomponent %}
- {% endif %}
- {% endcomponent %}
-
- {% component "unfold/components/card.html" %}
- {% component "unfold/components/title.html" %}
- {% trans "Most popular product" %}
- {% endcomponent %}
- {% if most_popular_products %}
-
- {% elif most_popular_product %}
-
- {% if most_popular_product.image %}
-
- {% endif %}
- {{ most_popular_product.name }}
-
- {% else %}
- {% component "unfold/components/text.html" %}
- {% trans "No data yet." %}
- {% endcomponent %}
- {% endif %}
- {% endcomponent %}
+ {% include "admin/dashboard/_customers_mix.html" %}
+ {% if shipped_vs_digital.digital_qty|default:0 > 0 and shipped_vs_digital.shipped_qty|default:0 > 0 %}
+ {% include "admin/dashboard/_shipped_vs_digital.html" %}
+ {% endif %}
+
+ {% include "admin/dashboard/_most_returned.html" %}
+ {% include "admin/dashboard/_top_categories.html" %}
+
+
+ {% include "admin/dashboard/_product_lists.html" %}
{% component "unfold/components/separator.html" %}
{% endcomponent %}
diff --git a/engine/core/utils/commerce.py b/engine/core/utils/commerce.py
index dbfd8ff5..47fe83e9 100644
--- a/engine/core/utils/commerce.py
+++ b/engine/core/utils/commerce.py
@@ -1,12 +1,14 @@
from contextlib import suppress
from datetime import date, timedelta
+from typing import Any
from constance import config
from django.db.models import Count, F, QuerySet, Sum
from django.db.models.functions import Coalesce, TruncDate
+from django.urls import reverse
from django.utils.timezone import now
-from engine.core.models import Order, OrderProduct
+from engine.core.models import Category, Order, OrderProduct, Product
def get_period_order_products(
@@ -110,3 +112,156 @@ def get_daily_gross_revenue(period: timedelta = timedelta(days=30)) -> dict[date
if d:
result[d] = total_f
return result
+
+
+def get_top_returned_products(period: timedelta = timedelta(days=30), limit: int = 10) -> list[dict[str, Any]]:
+ current = now()
+ period_start = current - period
+ qs = (
+ OrderProduct.objects.filter(
+ status="RETURNED",
+ order__status="FINISHED",
+ order__buy_time__lte=current,
+ order__buy_time__gte=period_start,
+ product__isnull=False,
+ )
+ .values("product")
+ .annotate(
+ returned_qty=Coalesce(Sum("quantity"), 0),
+ returned_amount=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0),
+ )
+ .order_by("-returned_qty")[:limit]
+ )
+
+ result: list[dict[str, Any]] = []
+ prod_ids = [row["product"] for row in qs if row.get("product")]
+ products = Product.objects.filter(pk__in=prod_ids)
+ product_by_id = {p.pk: p for p in products}
+ for row in qs:
+ pid = row.get("product")
+ if not pid or pid not in product_by_id:
+ continue
+ p = product_by_id[pid]
+ img = ""
+ with suppress(Exception):
+ img = p.images.first().image_url if p.images.exists() else "" # type: ignore [union-attr]
+ result.append(
+ {
+ "name": p.name,
+ "image": img,
+ "admin_url": reverse("admin:core_product_change", args=[p.pk]),
+ "count": int(row.get("returned_qty", 0) or 0),
+ "amount": round(float(row.get("returned_amount", 0.0) or 0.0), 2),
+ }
+ )
+ return result
+
+
+def get_customer_mix(period: timedelta = timedelta(days=30)) -> dict[str, int]:
+ current = now()
+ period_start = current - period
+ period_users = (
+ Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=period_start, user__isnull=False)
+ .values_list("user_id", flat=True)
+ .distinct()
+ )
+ if not period_users:
+ return {"new": 0, "returning": 0}
+
+ lifetime_counts = (
+ Order.objects.filter(status="FINISHED", user_id__in=period_users).values("user_id").annotate(c=Count("id"))
+ )
+ new_cnt = 0
+ ret_cnt = 0
+ for row in lifetime_counts:
+ c = int(row.get("c", 0) or 0)
+ if c <= 1:
+ new_cnt += 1
+ else:
+ ret_cnt += 1
+ return {"new": new_cnt, "returning": ret_cnt}
+
+
+def get_top_categories_by_qty(period: timedelta = timedelta(days=30), limit: int = 10) -> list[dict[str, Any]]:
+ current = now()
+ period_start = current - period
+ qs = (
+ OrderProduct.objects.filter(
+ status="FINISHED",
+ order__status="FINISHED",
+ order__buy_time__lte=current,
+ order__buy_time__gte=period_start,
+ product__isnull=False,
+ product__category__isnull=False,
+ )
+ .values("product__category")
+ .annotate(
+ qty=Coalesce(Sum("quantity"), 0),
+ gross=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0),
+ )
+ .order_by("-qty", "-gross")[:limit]
+ )
+
+ cat_ids = [row["product__category"] for row in qs if row.get("product__category")]
+ cats = Category.objects.filter(pk__in=cat_ids)
+ cat_by_id = {c.pk: c for c in cats}
+ result: list[dict[str, Any]] = []
+ for row in qs:
+ cid = row.get("product__category")
+ if not cid or cid not in cat_by_id:
+ continue
+ c = cat_by_id[cid]
+ result.append(
+ {
+ "name": c.name,
+ "admin_url": reverse("admin:core_category_change", args=[c.pk]),
+ "qty": int(row.get("qty", 0) or 0),
+ "gross": round(float(row.get("gross", 0.0) or 0.0), 2),
+ }
+ )
+ return result
+
+
+def get_shipped_vs_digital_mix(period: timedelta = timedelta(days=30)) -> dict[str, float | int]:
+ current = now()
+ period_start = current - period
+ qs = (
+ OrderProduct.objects.filter(
+ status="FINISHED",
+ order__status="FINISHED",
+ order__buy_time__lte=current,
+ order__buy_time__gte=period_start,
+ product__isnull=False,
+ )
+ .values("product__is_digital")
+ .annotate(
+ qty=Coalesce(Sum("quantity"), 0),
+ gross=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0),
+ )
+ )
+
+ digital_qty = 0
+ shipped_qty = 0
+ digital_gross = 0.0
+ shipped_gross = 0.0
+ for row in qs:
+ is_digital = bool(row.get("product__is_digital"))
+ q = int(row.get("qty", 0) or 0)
+ g = row.get("gross", 0.0) or 0.0
+ try:
+ g = float(g)
+ except (TypeError, ValueError):
+ g = 0.0
+ if is_digital:
+ digital_qty += q
+ digital_gross += g
+ else:
+ shipped_qty += q
+ shipped_gross += g
+
+ return {
+ "digital_qty": int(digital_qty),
+ "shipped_qty": int(shipped_qty),
+ "digital_gross": round(float(digital_gross), 2),
+ "shipped_gross": round(float(shipped_gross), 2),
+ }
diff --git a/engine/core/views.py b/engine/core/views.py
index aead8076..bb2cc86d 100644
--- a/engine/core/views.py
+++ b/engine/core/views.py
@@ -1,9 +1,9 @@
import logging
-from datetime import timedelta
import mimetypes
import os
import traceback
from contextlib import suppress
+from datetime import timedelta
import requests
from django.conf import settings
@@ -18,8 +18,8 @@ from django.template import Context
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import urlsafe_base64_decode
-from django.utils.translation import gettext_lazy as _
from django.utils.timezone import now as tz_now
+from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from django_ratelimit.decorators import ratelimit
@@ -60,11 +60,15 @@ from engine.core.serializers import (
from engine.core.utils import get_project_parameters, is_url_safe
from engine.core.utils.caching import web_cache
from engine.core.utils.commerce import (
- get_returns,
- get_revenue,
- get_total_processed_orders,
+ get_customer_mix,
get_daily_finished_orders_count,
get_daily_gross_revenue,
+ get_returns,
+ get_revenue,
+ get_shipped_vs_digital_mix,
+ get_top_categories_by_qty,
+ get_top_returned_products,
+ get_total_processed_orders,
)
from engine.core.utils.emailing import contact_us_email
from engine.core.utils.languages import get_flag_by_language
@@ -452,9 +456,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
}
)
- # Single most wished product (backward compatibility)
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):
wished_qs = (
@@ -475,12 +477,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
"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")
@@ -497,9 +497,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
}
)
- # Daily stats for the last 30 days
try:
- # Build continuous date axis
today = tz_now().date()
days = 30
date_axis = [today - timedelta(days=i) for i in range(days - 1, -1, -1)]
@@ -507,7 +505,6 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
orders_map = get_daily_finished_orders_count(timedelta(days=days))
gross_map = get_daily_gross_revenue(timedelta(days=days))
- # Labels in day-month format, e.g., 05 Nov
labels = [d.strftime("%d %b") for d in date_axis]
orders_series = [int(orders_map.get(d, 0) or 0) for d in date_axis]
gross_series = [float(gross_map.get(d, 0.0) or 0.0) for d in date_axis]
@@ -515,15 +512,20 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
context["daily_labels"] = labels
context["daily_orders"] = orders_series
context["daily_gross"] = gross_series
- except Exception as e: # pragma: no cover - fail safe
+ except Exception as e:
logger.warning("Failed to build daily stats: %s", e)
context["daily_labels"] = []
context["daily_orders"] = []
context["daily_gross"] = []
+ with suppress(Exception):
+ today = tz_now().date()
+ days = 30
+ date_axis = [today - timedelta(days=i) for i in range(days - 1, -1, -1)]
+ context["daily_labels"] = [d.strftime("%d %b") for d in date_axis]
+ context["daily_orders"] = [0 for _ in date_axis]
+ context["daily_gross"] = [0.0 for _ in date_axis]
- # Single most popular product (backward compatibility)
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):
popular_qs = (
@@ -563,6 +565,50 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
}
)
+ customers_mix: dict[str, int | float] = {"new": 0, "returning": 0, "new_pct": 0.0, "returning_pct": 0.0}
+ with suppress(Exception):
+ mix = get_customer_mix()
+ n = int(mix.get("new", 0))
+ r = int(mix.get("returning", 0))
+ t = max(n + r, 0)
+ new_pct = round((n / t * 100.0), 1) if t > 0 else 0.0
+ ret_pct = round((r / t * 100.0), 1) if t > 0 else 0.0
+ customers_mix = {"new": n, "returning": r, "new_pct": new_pct, "returning_pct": ret_pct, "total": t}
+
+ shipped_vs_digital: dict[str, int | float] = {
+ "digital_qty": 0,
+ "shipped_qty": 0,
+ "digital_gross": 0.0,
+ "shipped_gross": 0.0,
+ "digital_pct": 0.0,
+ "shipped_pct": 0.0,
+ }
+ with suppress(Exception):
+ svd = get_shipped_vs_digital_mix()
+ dq = int(svd.get("digital_qty", 0))
+ sq = int(svd.get("shipped_qty", 0))
+ total_q = dq + sq
+ digital_pct = round((dq / total_q * 100.0), 1) if total_q > 0 else 0.0
+ shipped_pct = round((sq / total_q * 100.0), 1) if total_q > 0 else 0.0
+ shipped_vs_digital.update(
+ {
+ "digital_qty": dq,
+ "shipped_qty": sq,
+ "digital_gross": float(svd.get("digital_gross", 0.0) or 0.0),
+ "shipped_gross": float(svd.get("shipped_gross", 0.0) or 0.0),
+ "digital_pct": digital_pct,
+ "shipped_pct": shipped_pct,
+ }
+ )
+
+ most_returned_products: list[dict[str, str | int | float]] = []
+ with suppress(Exception):
+ most_returned_products = get_top_returned_products()
+
+ top_categories: list[dict[str, str | int | float]] = []
+ with suppress(Exception):
+ top_categories = get_top_categories_by_qty()
+
context.update(
{
"custom_variable": "value",
@@ -577,6 +623,10 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
"most_wished_products": most_wished_list,
"most_popular_products": most_popular_list,
"currency_symbol": currency_symbol,
+ "customers_mix": customers_mix,
+ "shipped_vs_digital": shipped_vs_digital,
+ "most_returned_products": most_returned_products,
+ "top_categories": top_categories,
}
)
diff --git a/evibes/settings/jazzmin.py b/evibes/settings/jazzmin.py
deleted file mode 100644
index 44539e91..00000000
--- a/evibes/settings/jazzmin.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-Deprecated: Jazzmin settings (removed in favor of django-unfold).
-
-This file is intentionally left as a stub to avoid accidental imports.
-If imported, raise an explicit error guiding developers to Unfold.
-"""
-
-raise ImportError(
- "Jazzmin configuration has been removed. Use django-unfold instead. "
- "See evibes/settings/unfold.py and INSTALLED_APPS in evibes/settings/base.py."
-)