Features: 1) Added dashboard callback view to expose revenue, returns, and order metrics; 2) Added new admin dashboard template with visual metrics and charts; 3) Integrated language flags into UNFOLD configuration using centralized LANGUAGES_FLAGS.
Fixes: 1) None. Extra: 1) Refactored language flag definitions into a centralized dictionary in base settings; 2) Added commerce utility functions for revenue, returns, and order processing; 3) Improved code structure and documentation in views and utils.
This commit is contained in:
parent
555666d6fe
commit
56826300b6
5 changed files with 219 additions and 31 deletions
89
engine/core/templates/admin/index.html
Normal file
89
engine/core/templates/admin/index.html
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
{% extends 'admin/base.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if subtitle %}
|
||||||
|
{{ subtitle }} |
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ title }} | {{ site_title|default:_('Django site admin') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block branding %}
|
||||||
|
{% include "unfold/helpers/site_branding.html" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="px-4 py-6">
|
||||||
|
<h1 class="text-2xl font-semibold mb-6">{% trans "Dashboard" %}</h1>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-300">{% trans "Revenue (gross, 30d)" %}</div>
|
||||||
|
<div class="text-2xl font-bold mt-1">{{ revenue_gross_30|default:0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-300">{% trans "Revenue (net, 30d)" %}</div>
|
||||||
|
<div class="text-2xl font-bold mt-1">{{ revenue_net_30|default:0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-300">{% trans "Returns (30d)" %}</div>
|
||||||
|
<div class="text-2xl font-bold mt-1">{{ returns_30|default:0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-300">{% trans "Processed orders (30d)" %}</div>
|
||||||
|
<div class="text-2xl font-bold mt-1">{{ processed_orders_30|default:0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-center">
|
||||||
|
{% with gross=revenue_gross_30|default:0 returns=returns_30|default:0 total=gross|add:returns %}
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-lg font-semibold">{% trans "Sales vs Returns (30d)" %}</h2>
|
||||||
|
</div>
|
||||||
|
{% if total and total > 0 %}
|
||||||
|
{% widthratio gross total 360 as gross_deg %}
|
||||||
|
{% widthratio returns total 360 as returns_deg %}
|
||||||
|
<div class="flex flex-col sm:flex-row items-center gap-6">
|
||||||
|
<div class="relative w-40 h-40">
|
||||||
|
<div class="w-40 h-40 rounded-full"
|
||||||
|
style="background:
|
||||||
|
conic-gradient(
|
||||||
|
rgb(34,197,94) 0 {{ gross_deg }}deg,
|
||||||
|
rgb(239,68,68) {{ gross_deg }}deg 360deg
|
||||||
|
);">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="inline-block w-3 h-3 rounded-sm" style="background:rgb(34,197,94)"></span>
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Gross" %}:</span>
|
||||||
|
<span class="font-semibold">{{ gross }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="inline-block w-3 h-3 rounded-sm" style="background:rgb(239,68,68)"></span>
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Returns" %}:</span>
|
||||||
|
<span class="font-semibold">{{ returns }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-300">{% trans "Not enough data for chart yet." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-lg font-semibold mb-4">{% trans "Quick Links" %}</h2>
|
||||||
|
<ul class="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<li><a class="text-blue-600 hover:underline" href="{% url 'admin:core_order_changelist' %}">{% trans "Orders" %}</a></li>
|
||||||
|
<li><a class="text-blue-600 hover:underline" href="{% url 'admin:core_product_changelist' %}">{% trans "Products" %}</a></li>
|
||||||
|
<li><a class="text-blue-600 hover:underline" href="{% url 'admin:vibes_auth_user_changelist' %}">{% trans "Users" %}</a></li>
|
||||||
|
<li><a class="text-blue-600 hover:underline" href="{% url 'admin:django_celery_beat_periodictask_changelist' %}">{% trans "Tasks" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
52
engine/core/utils/commerce.py
Normal file
52
engine/core/utils/commerce.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.db.models import F, Sum
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from constance import config
|
||||||
|
from engine.core.models import Order, OrderProduct
|
||||||
|
|
||||||
|
|
||||||
|
def get_period_order_products(period: timedelta = timedelta(days=30), statuses: list[str] | None = None):
|
||||||
|
if statuses is None:
|
||||||
|
statuses = ["FINISHED"]
|
||||||
|
current = now()
|
||||||
|
perioded = current - period
|
||||||
|
orders = Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=perioded)
|
||||||
|
return OrderProduct.objects.filter(status__in=statuses, order__in=orders)
|
||||||
|
|
||||||
|
|
||||||
|
def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)):
|
||||||
|
order_products = get_period_order_products(period)
|
||||||
|
total: float = (
|
||||||
|
order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
total = float(total)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
total = 0.0
|
||||||
|
|
||||||
|
if clear:
|
||||||
|
try:
|
||||||
|
tax_rate = float(config.TAX_RATE or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
tax_rate = 0.0
|
||||||
|
net = total * (1 - tax_rate / 100.0)
|
||||||
|
return round(net, 2)
|
||||||
|
else:
|
||||||
|
return round(float(total), 2)
|
||||||
|
|
||||||
|
|
||||||
|
def get_returns(period: timedelta = timedelta(days=30)):
|
||||||
|
order_products = get_period_order_products(period, ["RETURNED"])
|
||||||
|
total_returns: float = (
|
||||||
|
order_products.aggregate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0)).get("total") or 0.0
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return round(float(total_returns), 2)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def get_total_processed_orders(period: timedelta = timedelta(days=30)):
|
||||||
|
return get_period_order_products(period, ["RETURNED", "FINISHED"]).count()
|
||||||
|
|
@ -52,6 +52,7 @@ from engine.core.serializers import (
|
||||||
LanguageSerializer,
|
LanguageSerializer,
|
||||||
)
|
)
|
||||||
from engine.core.utils import get_project_parameters, is_url_safe
|
from engine.core.utils import get_project_parameters, is_url_safe
|
||||||
|
from engine.core.utils.commerce import get_revenue, get_returns, get_total_processed_orders
|
||||||
from engine.core.utils.caching import web_cache
|
from engine.core.utils.caching import web_cache
|
||||||
from engine.core.utils.emailing import contact_us_email
|
from engine.core.utils.emailing import contact_us_email
|
||||||
from engine.core.utils.languages import get_flag_by_language
|
from engine.core.utils.languages import get_flag_by_language
|
||||||
|
|
@ -410,3 +411,27 @@ def version(request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
version.__doc__ = _( # type: ignore [assignment]
|
version.__doc__ = _( # type: ignore [assignment]
|
||||||
"Returns current version of the eVibes. "
|
"Returns current version of the eVibes. "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard_callback(request, context):
|
||||||
|
revenue_gross_30 = get_revenue(clear=False)
|
||||||
|
revenue_net_30 = get_revenue(clear=True)
|
||||||
|
returns_30 = get_returns()
|
||||||
|
processed_orders_30 = get_total_processed_orders()
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
dashboard_callback.__doc__ = _( # type: ignore [assignment]
|
||||||
|
"Returns custom variables for Dashboard. "
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,37 @@ LANGUAGES: tuple[tuple[str, str], ...] = (
|
||||||
("zh-hans", "简体中文"),
|
("zh-hans", "简体中文"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LANGUAGES_FLAGS: dict[str, str] = {
|
||||||
|
"ar-ar": "🇸🇦",
|
||||||
|
"cs-cz": "🇨🇿",
|
||||||
|
"da-dk": "🇩🇰",
|
||||||
|
"de-de": "🇩🇪",
|
||||||
|
"en-gb": "🇬🇧",
|
||||||
|
"en-us": "🇺🇸",
|
||||||
|
"es-es": "🇪🇸",
|
||||||
|
"fa-ir": "🇮🇷",
|
||||||
|
"fr-fr": "🇫🇷",
|
||||||
|
"he-il": "🇮🇱",
|
||||||
|
"hi-in": "🇮🇳",
|
||||||
|
"hr-hr": "🇭🇷",
|
||||||
|
"id-id": "🇮🇩",
|
||||||
|
"it-it": "🇮🇹",
|
||||||
|
"ja-jp": "🇯🇵",
|
||||||
|
"kk-kz": "🇰🇿",
|
||||||
|
"ko-kr": "🇰🇷",
|
||||||
|
"nl-nl": "🇳🇱",
|
||||||
|
"no-no": "🇳🇴",
|
||||||
|
"pl-pl": "🇵🇱",
|
||||||
|
"pt-br": "🇧🇷",
|
||||||
|
"ro-ro": "🇷🇴",
|
||||||
|
"ru-ru": "🇷🇺",
|
||||||
|
"sv-se": "🇸🇪",
|
||||||
|
"th-th": "🇹🇭",
|
||||||
|
"tr-tr": "🇹🇷",
|
||||||
|
"vi-vn": "🇻🇳",
|
||||||
|
"zh-hans": "🇨🇳",
|
||||||
|
}
|
||||||
|
|
||||||
LANGUAGE_CODE: str = "en-gb"
|
LANGUAGE_CODE: str = "en-gb"
|
||||||
|
|
||||||
CURRENCIES_BY_LANGUAGES: tuple[tuple[str, str], ...] = (
|
CURRENCIES_BY_LANGUAGES: tuple[tuple[str, str], ...] = (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,14 @@ from django.templatetags.static import static
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from evibes.settings.base import PROJECT_NAME, STOREFRONT_DOMAIN, SUPPORT_CONTACT, TASKBOARD_URL
|
from evibes.settings.base import (
|
||||||
|
PROJECT_NAME,
|
||||||
|
STOREFRONT_DOMAIN,
|
||||||
|
SUPPORT_CONTACT,
|
||||||
|
TASKBOARD_URL,
|
||||||
|
LANGUAGES as BASE_LANGUAGES,
|
||||||
|
LANGUAGES_FLAGS,
|
||||||
|
)
|
||||||
|
|
||||||
UNFOLD = {
|
UNFOLD = {
|
||||||
"SITE_URL": STOREFRONT_DOMAIN,
|
"SITE_URL": STOREFRONT_DOMAIN,
|
||||||
|
|
@ -20,38 +27,22 @@ UNFOLD = {
|
||||||
"search_models": True,
|
"search_models": True,
|
||||||
"show_history": True,
|
"show_history": True,
|
||||||
},
|
},
|
||||||
|
"DASHBOARD_CALLBACK": "core.views.dashboard_callback",
|
||||||
|
"LANGUAGES": {
|
||||||
|
"navigation": [
|
||||||
|
{
|
||||||
|
"bidi": code.split("-")[0] in {"ar", "he", "fa", "ur"},
|
||||||
|
"code": code,
|
||||||
|
"name": LANGUAGES_FLAGS.get(code),
|
||||||
|
"name_local": name,
|
||||||
|
"name_translated": _(name),
|
||||||
|
}
|
||||||
|
for code, name in BASE_LANGUAGES
|
||||||
|
],
|
||||||
|
},
|
||||||
"EXTENSIONS": {
|
"EXTENSIONS": {
|
||||||
"modeltranslation": {
|
"modeltranslation": {
|
||||||
"flags": {
|
"flags": LANGUAGES_FLAGS,
|
||||||
"ar-ar": "🇸🇦",
|
|
||||||
"cs-cz": "🇨🇿",
|
|
||||||
"da-dk": "🇩🇰",
|
|
||||||
"de-de": "🇩🇪",
|
|
||||||
"en-gb": "🇬🇧",
|
|
||||||
"en-us": "🇺🇸",
|
|
||||||
"es-es": "🇪🇸",
|
|
||||||
"fa-ir": "🇮🇷",
|
|
||||||
"fr-fr": "🇫🇷",
|
|
||||||
"he-il": "🇮🇱",
|
|
||||||
"hi-in": "🇮🇳",
|
|
||||||
"hr-hr": "🇭🇷",
|
|
||||||
"id-id": "🇮🇩",
|
|
||||||
"it-it": "🇮🇹",
|
|
||||||
"ja-jp": "🇯🇵",
|
|
||||||
"kk-kz": "🇰🇿",
|
|
||||||
"ko-kr": "🇰🇷",
|
|
||||||
"nl-nl": "🇳🇱",
|
|
||||||
"no-no": "🇳🇴",
|
|
||||||
"pl-pl": "🇵🇱",
|
|
||||||
"pt-br": "🇧🇷",
|
|
||||||
"ro-ro": "🇷🇴",
|
|
||||||
"ru-ru": "🇷🇺",
|
|
||||||
"sv-se": "🇸🇪",
|
|
||||||
"th-th": "🇹🇭",
|
|
||||||
"tr-tr": "🇹🇷",
|
|
||||||
"vi-vn": "🇻🇳",
|
|
||||||
"zh-hans": "🇨🇳",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"SIDEBAR": {
|
"SIDEBAR": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue