Features: 1) Add timeframe filter component in the admin dashboard; 2) Create low stock products widget with tailored design; 3) Extend KPI section to include dynamic data (GMV, Orders, AOV, Net Revenue, Refund Rate); 4) Allow timeframe selection functionality for metrics and charts.
Fixes: 1) Add missing `constance.config` import in `views.py`; 2) Replace hardcoded timeframe logic with configurable period parsing; 3) Ensure proper handling of empty datasets for low stock and KPI calculations. Extra: Refactor dashboard templates to improve layout and add contextual adjustments (e.g., grid updates, daily sales title); Optimize cache handling for dashboard metrics; Cleanup unused legacy dashboard variables.
This commit is contained in:
parent
d03b6b0ec9
commit
5962fb1145
7 changed files with 298 additions and 33 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Daily sales (30d)" %}
|
||||
{{ daily_title|default:_('Daily sales') }}
|
||||
{% endcomponent %}
|
||||
<div class="w-full">
|
||||
<canvas id="dailySalesChart" height="120"></canvas>
|
||||
|
|
|
|||
22
engine/core/templates/admin/dashboard/_filters.html
Normal file
22
engine/core/templates/admin/dashboard/_filters.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{% load i18n unfold %}
|
||||
|
||||
{% component "unfold/components/card.html" with class="mb-4" %}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{% component "unfold/components/text.html" with class="text-sm text-gray-500 dark:text-gray-400 mr-2" %}
|
||||
{% trans "Timeframe" %}
|
||||
{% endcomponent %}
|
||||
{% with cur=tf|default:30 %}
|
||||
<a href="?tf=7"
|
||||
class="px-3 py-1 rounded-full text-sm border transition-colors {% if cur == 7 %} bg-gray-900 text-white dark:bg-white dark:text-gray-900 border-transparent {% else %} border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 {% endif %}">7d</a>
|
||||
<a href="?tf=30"
|
||||
class="px-3 py-1 rounded-full text-sm border transition-colors {% if cur == 30 %} bg-gray-900 text-white dark:bg-white dark:text-gray-900 border-transparent {% else %} border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 {% endif %}">30d</a>
|
||||
<a href="?tf=90"
|
||||
class="px-3 py-1 rounded-full text-sm border transition-colors {% if cur == 90 %} bg-gray-900 text-white dark:bg-white dark:text-gray-900 border-transparent {% else %} border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 {% endif %}">90d</a>
|
||||
<a href="?tf=360"
|
||||
class="px-3 py-1 rounded-full text-sm border transition-colors {% if cur == 360 %} bg-gray-900 text-white dark:bg-white dark:text-gray-900 border-transparent {% else %} border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 {% endif %}">360d</a>
|
||||
{% endwith %}
|
||||
<div class="ml-auto text-xs text-gray-400">
|
||||
{% trans "TODO: Integrate GA/Yandex.Metrica & Ads metrics" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endcomponent %}
|
||||
|
|
@ -1,39 +1,86 @@
|
|||
{% load i18n unfold %}
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
||||
{#
|
||||
KPI cards (mobile-first): GMV, Orders, AOV, Net revenue, Refund rate
|
||||
Uses `kpi` dict and `currency_symbol` provided by dashboard_callback
|
||||
#}
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 mb-4">
|
||||
{# GMV #}
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Revenue (gross, 30d)" %}
|
||||
{% endcomponent %}
|
||||
<div class="flex items-start justify-between">
|
||||
{% component "unfold/components/text.html" %}{% trans "GMV" %} ({{ tf|default:30 }}d){% endcomponent %}
|
||||
{% with d=kpi.gmv.delta_pct|default:0 %}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full {% if d >= 0 %} bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 {% else %} bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 {% endif %}">
|
||||
{% if d >= 0 %}+{% endif %}{{ d }}%
|
||||
</span>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_gross_30|default:0 }}
|
||||
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ kpi.gmv.value|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{# Orders #}
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Revenue (net, 30d)" %}
|
||||
{% endcomponent %}
|
||||
<div class="flex items-start justify-between">
|
||||
{% component "unfold/components/text.html" %}{% trans "Orders" %} ({{ tf|default:30 }}d){% endcomponent %}
|
||||
{% with d=kpi.orders.delta_pct|default:0 %}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full {% if d >= 0 %} bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 {% else %} bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 {% endif %}">
|
||||
{% if d >= 0 %}+{% endif %}{{ d }}%
|
||||
</span>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ revenue_net_30|default:0 }}
|
||||
{{ kpi.orders.value|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{# AOV #}
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Returns (30d)" %}
|
||||
{% endcomponent %}
|
||||
<div class="flex items-start justify-between">
|
||||
{% component "unfold/components/text.html" %}{% trans "AOV" %} ({{ tf|default:30 }}d){% endcomponent %}
|
||||
{% with d=kpi.aov.delta_pct|default:0 %}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full {% if d >= 0 %} bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 {% else %} bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 {% endif %}">
|
||||
{% if d >= 0 %}+{% endif %}{{ d }}%
|
||||
</span>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns_30|default:0 }}
|
||||
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ kpi.aov.value|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{# Net revenue #}
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Processed orders (30d)" %}
|
||||
{% endcomponent %}
|
||||
<div class="flex items-start justify-between">
|
||||
{% component "unfold/components/text.html" %}{% trans "Net revenue" %} ({{ tf|default:30 }}d){% endcomponent %}
|
||||
{% with d=kpi.net.delta_pct|default:0 %}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full {% if d >= 0 %} bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 {% else %} bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 {% endif %}">
|
||||
{% if d >= 0 %}+{% endif %}{{ d }}%
|
||||
</span>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% component "unfold/components/title.html" %}
|
||||
{{ processed_orders_30|default:0 }}
|
||||
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ kpi.net.value|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
|
||||
{# Refund rate #}
|
||||
{% component "unfold/components/card.html" %}
|
||||
<div class="flex items-start justify-between">
|
||||
{% component "unfold/components/text.html" %}{% trans "Refund rate" %} ({{ tf|default:30 }}d){% endcomponent %}
|
||||
{% with d=kpi.refund_rate.delta_pct|default:0 %}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full {% if d <= 0 %} bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 {% else %} bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300 {% endif %}">
|
||||
{% if d >= 0 %}+{% endif %}{{ d }}%
|
||||
</span>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% component "unfold/components/title.html" %}
|
||||
{{ kpi.refund_rate.value|default:0 }}%
|
||||
{% endcomponent %}
|
||||
{% component "unfold/components/text.html" with class="text-xs text-gray-500 dark:text-gray-400" %}
|
||||
{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns_amount|default:0 }} {% trans "returned" %}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
|
|
|||
32
engine/core/templates/admin/dashboard/_low_stock.html
Normal file
32
engine/core/templates/admin/dashboard/_low_stock.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{% load i18n unfold %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Low stock" %}
|
||||
{% endcomponent %}
|
||||
|
||||
{% if low_stock_products %}
|
||||
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{% for p in low_stock_products %}
|
||||
<a href="{{ p.admin_url }}" class="flex items-center justify-between py-2 hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-md px-2 -mx-2">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium">{{ p.name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">SKU: {{ p.sku }}</div>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs
|
||||
{% if p.qty|default:0 <= 0 %} bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300
|
||||
{% elif p.qty <= 5 %} bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300
|
||||
{% else %} bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200 {% endif %}">
|
||||
{{ p.qty }}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" with class="text-sm text-gray-500 dark:text-gray-400" %}
|
||||
{% trans "No low stock items." %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
|
|
@ -21,14 +21,21 @@
|
|||
<br/>
|
||||
{% endcomponent %}
|
||||
|
||||
{% include "admin/dashboard/_filters.html" %}
|
||||
|
||||
{% include "admin/dashboard/_kpis.html" %}
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 items-start">
|
||||
<div class="xl:col-span-2">
|
||||
{% include "admin/dashboard/_daily_sales.html" %}
|
||||
</div>
|
||||
{% include "admin/dashboard/_income_overview.html" %}
|
||||
{% include "admin/dashboard/_quick_links.html" %}
|
||||
</div>
|
||||
|
||||
{% include "admin/dashboard/_daily_sales.html" %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||
{% include "admin/dashboard/_low_stock.html" %}
|
||||
{% include "admin/dashboard/_most_returned.html" %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||
{% include "admin/dashboard/_customers_mix.html" %}
|
||||
|
|
@ -38,8 +45,8 @@
|
|||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||
{% include "admin/dashboard/_most_returned.html" %}
|
||||
{% include "admin/dashboard/_top_categories.html" %}
|
||||
{% include "admin/dashboard/_quick_links.html" %}
|
||||
</div>
|
||||
|
||||
{% include "admin/dashboard/_product_lists.html" %}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@ import mimetypes
|
|||
import os
|
||||
import traceback
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
from datetime import date, timedelta
|
||||
|
||||
import requests
|
||||
from constance import config
|
||||
from django.conf import settings
|
||||
from django.contrib.sitemaps.views import index as _sitemap_index_view
|
||||
from django.contrib.sitemaps.views import sitemap as _sitemap_detail_view
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.db.models import Count, Sum
|
||||
from django.db.models import Count, Sum, F
|
||||
from django.http import FileResponse, Http404, HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template import Context
|
||||
|
|
@ -430,10 +431,117 @@ version.__doc__ = _( # type: ignore [assignment]
|
|||
|
||||
|
||||
def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
||||
revenue_gross_30: float = get_revenue(clear=False)
|
||||
revenue_net_30: float = get_revenue(clear=True)
|
||||
returns_30: float = get_returns()
|
||||
processed_orders_30: int = get_total_processed_orders()
|
||||
tf_map: dict[str, int] = {"7": 7, "30": 30, "90": 90, "360": 360}
|
||||
tf_param = str(request.GET.get("tf", "30") or "30")
|
||||
period_days: int = tf_map.get(tf_param, 30)
|
||||
period = timedelta(days=period_days)
|
||||
|
||||
now_dt = tz_now()
|
||||
cur_start = now_dt - period
|
||||
prev_start = now_dt - timedelta(days=period_days * 2)
|
||||
prev_end = cur_start
|
||||
|
||||
revenue_gross_cur: float = get_revenue(clear=False, period=period)
|
||||
revenue_net_cur: float = get_revenue(clear=True, period=period)
|
||||
returns_cur: float = get_returns(period=period)
|
||||
processed_orders_cur: int = get_total_processed_orders(period=period)
|
||||
|
||||
orders_finished_cur = 0
|
||||
|
||||
with suppress(Exception):
|
||||
orders_finished_cur: int = Order.objects.filter(
|
||||
status="FINISHED", buy_time__lte=now_dt, buy_time__gte=cur_start
|
||||
).count()
|
||||
|
||||
def sum_gross_between(start: date | None, end: date | None) -> float:
|
||||
result = 0.0
|
||||
with suppress(Exception):
|
||||
qs = (
|
||||
OrderProduct.objects.filter(status__in=["FINISHED"], order__status="FINISHED")
|
||||
.filter(order__buy_time__lt=end, order__buy_time__gte=start)
|
||||
.aggregate(total=Sum(F("buy_price") * F("quantity")))
|
||||
)
|
||||
total = qs.get("total") or 0.0
|
||||
result = round(float(total), 2)
|
||||
return result
|
||||
|
||||
def sum_returns_between(start: date | None, end: date | None) -> float:
|
||||
result = 0.0
|
||||
with suppress(Exception):
|
||||
qs = (
|
||||
OrderProduct.objects.filter(status__in=["RETURNED"]) # returned items
|
||||
.filter(order__buy_time__lt=end, order__buy_time__gte=start)
|
||||
.aggregate(total=Sum(F("buy_price") * F("quantity")))
|
||||
)
|
||||
total = qs.get("total") or 0.0
|
||||
result = round(float(total), 2)
|
||||
return result
|
||||
|
||||
def count_finished_orders_between(start: date | None, end: date | None) -> int:
|
||||
result = 0
|
||||
with suppress(Exception):
|
||||
result = Order.objects.filter(status="FINISHED", buy_time__lt=end, buy_time__gte=start).count()
|
||||
return result
|
||||
|
||||
revenue_gross_prev = sum_gross_between(prev_start, prev_end)
|
||||
returns_prev = sum_returns_between(prev_start, prev_end)
|
||||
orders_finished_prev = count_finished_orders_between(prev_start, prev_end)
|
||||
|
||||
tax_rate = 0.0
|
||||
tax_included = False
|
||||
with suppress(Exception):
|
||||
tax_rate = float(getattr(config, "TAX_RATE", 0.0) or 0.0)
|
||||
tax_included = bool(getattr(config, "TAX_INCLUDED", False))
|
||||
|
||||
if tax_rate <= 0:
|
||||
revenue_net_prev = revenue_gross_prev
|
||||
else:
|
||||
if tax_included:
|
||||
divisor = 1.0 + (tax_rate / 100.0)
|
||||
revenue_net_prev = revenue_gross_prev / divisor if divisor > 0 else revenue_gross_prev
|
||||
else:
|
||||
revenue_net_prev = revenue_gross_prev
|
||||
revenue_net_prev = round(float(revenue_net_prev or 0.0), 2)
|
||||
|
||||
def pct_delta(cur: float | int, prev: float | int) -> float:
|
||||
result = 0.0
|
||||
with suppress(Exception):
|
||||
cur_f = float(cur or 0)
|
||||
prev_f = float(prev or 0)
|
||||
if prev_f == 0:
|
||||
result = 0.0 if cur_f == 0 else 100.0
|
||||
result = round(((cur_f - prev_f) / prev_f) * 100.0, 1)
|
||||
return result
|
||||
|
||||
aov_cur: float = round((revenue_gross_cur / orders_finished_cur), 2) if orders_finished_cur > 0 else 0.0
|
||||
refund_rate_cur: float = round(((returns_cur / revenue_gross_cur) * 100.0), 1) if revenue_gross_cur > 0 else 0.0
|
||||
|
||||
aov_prev: float = round((revenue_gross_prev / orders_finished_prev), 2) if orders_finished_prev > 0 else 0.0
|
||||
refund_rate_prev: float = round(((returns_prev / revenue_gross_prev) * 100.0), 1) if revenue_gross_prev > 0 else 0.0
|
||||
|
||||
kpi = {
|
||||
"gmv": {
|
||||
"value": revenue_gross_cur,
|
||||
"prev": revenue_gross_prev,
|
||||
"delta_pct": pct_delta(revenue_gross_cur, revenue_gross_prev),
|
||||
},
|
||||
"orders": {
|
||||
"value": orders_finished_cur,
|
||||
"prev": orders_finished_prev,
|
||||
"delta_pct": pct_delta(orders_finished_cur, orders_finished_prev),
|
||||
},
|
||||
"aov": {"value": aov_cur, "prev": aov_prev, "delta_pct": pct_delta(aov_cur, aov_prev)},
|
||||
"net": {
|
||||
"value": revenue_net_cur,
|
||||
"prev": revenue_net_prev,
|
||||
"delta_pct": pct_delta(revenue_net_cur, revenue_net_prev),
|
||||
},
|
||||
"refund_rate": {
|
||||
"value": refund_rate_cur,
|
||||
"prev": refund_rate_prev,
|
||||
"delta_pct": pct_delta(refund_rate_cur, refund_rate_prev),
|
||||
},
|
||||
}
|
||||
currency_symbol: str = ""
|
||||
with suppress(Exception):
|
||||
currency_symbol = dict(getattr(settings, "CURRENCIES_WITH_SYMBOLS", ())).get(
|
||||
|
|
@ -499,7 +607,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
|
||||
try:
|
||||
today = tz_now().date()
|
||||
days = 30
|
||||
days = period_days
|
||||
date_axis = [today - timedelta(days=i) for i in range(days - 1, -1, -1)]
|
||||
|
||||
orders_map = get_daily_finished_orders_count(timedelta(days=days))
|
||||
|
|
@ -512,6 +620,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
context["daily_labels"] = labels
|
||||
context["daily_orders"] = orders_series
|
||||
context["daily_gross"] = gross_series
|
||||
context["daily_title"] = f"Revenue & Orders (last {period_days}d)"
|
||||
except Exception as e:
|
||||
logger.warning("Failed to build daily stats: %s", e)
|
||||
context["daily_labels"] = []
|
||||
|
|
@ -519,11 +628,48 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
context["daily_gross"] = []
|
||||
with suppress(Exception):
|
||||
today = tz_now().date()
|
||||
days = 30
|
||||
days = period_days
|
||||
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]
|
||||
context["daily_title"] = f"Revenue & Orders (last {period_days}d)"
|
||||
|
||||
low_stock_list: list[dict[str, str | int]] = []
|
||||
with suppress(Exception):
|
||||
products = (
|
||||
Product.objects.annotate(total_qty=Sum("stocks__quantity"))
|
||||
.values("id", "name", "sku", "total_qty")
|
||||
.order_by("total_qty")[:5]
|
||||
)
|
||||
for p in products:
|
||||
qty = int(p.get("total_qty") or 0)
|
||||
low_stock_list.append(
|
||||
{
|
||||
"name": str(p.get("name") or ""),
|
||||
"sku": str(p.get("sku") or ""),
|
||||
"qty": qty,
|
||||
"admin_url": reverse("admin:core_product_change", args=[p.get("id")]),
|
||||
}
|
||||
)
|
||||
|
||||
cache_key = f"dashboard_cb:{period_days}"
|
||||
cached_pack = None
|
||||
with suppress(Exception):
|
||||
cached_pack = cache.get(cache_key)
|
||||
if cached_pack is None:
|
||||
cached_pack = {
|
||||
"kpi": kpi,
|
||||
"revenue_gross": revenue_gross_cur,
|
||||
"revenue_net": revenue_net_cur,
|
||||
"returns_amount": returns_cur,
|
||||
"orders_finished": orders_finished_cur,
|
||||
"aov": aov_cur,
|
||||
"refund_rate": refund_rate_cur,
|
||||
"low_stock_products": low_stock_list,
|
||||
}
|
||||
with suppress(Exception):
|
||||
cache.set(cache_key, cached_pack, 600)
|
||||
|
||||
most_popular: dict[str, str | int | float | None] | None = None
|
||||
most_popular_list: list[dict[str, str | int | float | None]] = []
|
||||
|
|
@ -612,10 +758,19 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
context.update(
|
||||
{
|
||||
"custom_variable": "value",
|
||||
"revenue_gross_30": revenue_gross_30,
|
||||
"revenue_net_30": revenue_net_30,
|
||||
"returns_30": returns_30,
|
||||
"processed_orders_30": processed_orders_30,
|
||||
"timeframe_days": period_days,
|
||||
"tf": period_days,
|
||||
"kpi": kpi,
|
||||
"revenue_gross": revenue_gross_cur,
|
||||
"revenue_net": revenue_net_cur,
|
||||
"returns_amount": returns_cur,
|
||||
"orders_finished": orders_finished_cur,
|
||||
"aov": aov_cur,
|
||||
"refund_rate": refund_rate_cur,
|
||||
"revenue_gross_30": revenue_gross_cur,
|
||||
"revenue_net_30": revenue_net_cur,
|
||||
"returns_30": returns_cur,
|
||||
"processed_orders_30": processed_orders_cur,
|
||||
"evibes_version": settings.EVIBES_VERSION,
|
||||
"quick_links": quick_links,
|
||||
"most_wished_product": most_wished,
|
||||
|
|
@ -627,6 +782,7 @@ def dashboard_callback(request: HttpRequest, context: Context) -> Context:
|
|||
"shipped_vs_digital": shipped_vs_digital,
|
||||
"most_returned_products": most_returned_products,
|
||||
"top_categories": top_categories,
|
||||
"low_stock_products": low_stock_list,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from engine.vibes_auth.models import User
|
|||
|
||||
class UserForm(UserChangeForm): # type: ignore [type-arg]
|
||||
password = ReadOnlyPasswordHashField(label="Password")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = "__all__"
|
||||
|
|
|
|||
Loading…
Reference in a new issue