Merge branch 'main' into storefront-nuxt
This commit is contained in:
commit
7907613fdb
5 changed files with 429 additions and 167 deletions
14
engine/core/static/js/chart.js
Normal file
14
engine/core/static/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% load i18n unfold %}
|
||||
{% load i18n unfold static %}
|
||||
|
||||
{% block title %}
|
||||
{% if subtitle %}
|
||||
|
|
@ -16,184 +16,341 @@
|
|||
{% block content %}
|
||||
{% component "unfold/components/container.html" %}
|
||||
<div class="flex flex-col min-h-screen">
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Dashboard" %}
|
||||
<br/>
|
||||
{% endcomponent %}
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
||||
{% 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 %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Dashboard" %}
|
||||
<br/>
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Revenue (net, 30d)" %}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
||||
{% 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/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)" %}
|
||||
{% 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/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)" %}
|
||||
{% 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/title.html" %}
|
||||
{{ processed_orders_30|default:0 }}
|
||||
{% endcomponent %}
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 items-start">
|
||||
{% 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 "Sales vs Returns (30d)" %}
|
||||
{% endcomponent %}
|
||||
{% if total and total > 0 %}
|
||||
{% widthratio gross total 360 as gross_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">{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ 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">{% if currency_symbol %}{{ currency_symbol }}{% endif %}{{ returns }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "Not enough data for chart yet." %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 items-start">
|
||||
{% 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 "Sales vs Returns (30d)" %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
{% if total and total > 0 %}
|
||||
{% with net=revenue_net_30|default:0 %}
|
||||
{% with tax_amt=gross|add:-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|add:-tax_amt_pos|add:-returns_capped %}
|
||||
{% if net_for_pie < 0 %}
|
||||
{% with net_for_pie=0 %}{% endwith %}
|
||||
{% endif %}
|
||||
{# Degrees per slice #}
|
||||
{% 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 %}
|
||||
<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 {{ net_end }}deg,
|
||||
rgb(249,115,22) {{ net_end }}deg {{ tax_end }}deg,
|
||||
rgb(239,68,68) {{ tax_end }}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 "Net" %}:</span>
|
||||
<span class="font-semibold">{% if currency_symbol %}
|
||||
{{ currency_symbol }}{% endif %}{{ net }}</span>
|
||||
</div>
|
||||
{% if tax_amt_pos > 0 %}
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm"
|
||||
style="background:rgb(249,115,22)"></span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Taxes" %}:</span>
|
||||
<span class="font-semibold">{% if currency_symbol %}
|
||||
{{ currency_symbol }}{% endif %}{{ tax_amt_pos|floatformat:2 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex items-center gap-2 mb-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">{% if currency_symbol %}
|
||||
{{ currency_symbol }}{% endif %}{{ returns }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm"
|
||||
style="background:linear-gradient(90deg, rgba(0,0,0,0.15), rgba(0,0,0,0.15))"></span>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">{% trans "Gross (pie total)" %}:</span>
|
||||
<span class="font-semibold">{% if currency_symbol %}
|
||||
{{ currency_symbol }}{% endif %}{{ gross }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Quick Links" %}
|
||||
{% trans "Daily sales (30d)" %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% if daily_labels and daily_labels|length > 0 %}
|
||||
<div class="w-full">
|
||||
<canvas id="dailySalesChart" height="120"></canvas>
|
||||
</div>
|
||||
<script src="{% static 'js/chart.js' %}"></script>
|
||||
{{ daily_labels|json_script:"daily-labels" }}
|
||||
{{ daily_orders|json_script:"daily-orders" }}
|
||||
{{ daily_gross|json_script:"daily-gross" }}
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
const labels = JSON.parse(document.getElementById('daily-labels').textContent);
|
||||
const orders = JSON.parse(document.getElementById('daily-orders').textContent);
|
||||
const gross = JSON.parse(document.getElementById('daily-gross').textContent);
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Most wished product" %}
|
||||
{% endcomponent %}
|
||||
{% 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">
|
||||
{% if most_wished_product.image %}
|
||||
<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>
|
||||
</a>
|
||||
const ctx = document.getElementById('dailySalesChart').getContext('2d');
|
||||
const green = 'rgb(34,197,94)';
|
||||
const blue = 'rgb(59,130,246)';
|
||||
|
||||
const currency = "{% if currency_symbol %}{{ currency_symbol }}{% endif %}";
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '{{ _("Orders (FINISHED)") }}',
|
||||
data: orders,
|
||||
borderColor: green,
|
||||
backgroundColor: green,
|
||||
borderWidth: 2,
|
||||
tension: 0.25,
|
||||
pointRadius: 2,
|
||||
yAxisID: 'yOrders',
|
||||
},
|
||||
{
|
||||
label: '{{ _("Gross revenue") }}',
|
||||
data: gross,
|
||||
borderColor: blue,
|
||||
backgroundColor: blue,
|
||||
borderWidth: 2,
|
||||
tension: 0.25,
|
||||
pointRadius: 2,
|
||||
yAxisID: 'yRevenue',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {mode: 'index', intersect: false},
|
||||
plugins: {
|
||||
legend: {display: true},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
const label = context.dataset.label || '';
|
||||
const val = context.parsed.y;
|
||||
if (context.dataset.yAxisID === 'yRevenue') {
|
||||
return `${label}: ${currency}${val}`;
|
||||
}
|
||||
return `${label}: ${val}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {display: false}
|
||||
},
|
||||
yOrders: {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
title: {display: true, text: '{{ _("Orders") }}'},
|
||||
grid: {color: 'rgba(0,0,0,0.06)'},
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
yRevenue: {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
title: {display: true, text: '{{ _("Gross") }}'},
|
||||
grid: {drawOnChartArea: false},
|
||||
ticks: {
|
||||
callback: function (value) {
|
||||
return `${currency}${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console && console.warn && console.warn('Chart init failed', e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "No data yet." %}
|
||||
{% trans "Not enough data for chart yet." %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Most popular product" %}
|
||||
{% endcomponent %}
|
||||
{% 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">
|
||||
{% if most_popular_product.image %}
|
||||
<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>
|
||||
</a>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "No data yet." %}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||
{% component "unfold/components/card.html" %}
|
||||
{% component "unfold/components/title.html" %}
|
||||
{% trans "Most wished product" %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% 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">
|
||||
{% if most_wished_product.image %}
|
||||
<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>
|
||||
</a>
|
||||
{% 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 %}
|
||||
<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">
|
||||
{% if most_popular_product.image %}
|
||||
<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>
|
||||
</a>
|
||||
{% else %}
|
||||
{% component "unfold/components/text.html" %}
|
||||
{% trans "No data yet." %}
|
||||
{% endcomponent %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
|
||||
{% component "unfold/components/separator.html" %}
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
|
||||
{% component "unfold/components/separator.html" %}
|
||||
{% endcomponent %}
|
||||
|
||||
<div class="mt-4 mt-auto">
|
||||
{% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %}
|
||||
eVibes {{ evibes_version }} · Wiseless Team
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
<div class="mt-4 mt-auto">
|
||||
{% component "unfold/components/text.html" with class="text-center text-xs text-gray-500 dark:text-gray-400" %}
|
||||
eVibes {{ evibes_version }} · Wiseless Team
|
||||
{% endcomponent %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endcomponent %}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from datetime import timedelta
|
||||
from contextlib import suppress
|
||||
from datetime import date, timedelta
|
||||
|
||||
from constance import config
|
||||
from django.db.models import F, QuerySet, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import Count, F, QuerySet, Sum
|
||||
from django.db.models.functions import Coalesce, TruncDate
|
||||
from django.utils.timezone import now
|
||||
|
||||
from engine.core.models import Order, OrderProduct
|
||||
|
|
@ -29,16 +30,32 @@ def get_revenue(clear: bool = True, period: timedelta = timedelta(days=30)) -> f
|
|||
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:
|
||||
if not clear:
|
||||
return round(float(total), 2)
|
||||
|
||||
try:
|
||||
tax_rate = float(config.TAX_RATE or 0)
|
||||
except (TypeError, ValueError):
|
||||
tax_rate = 0.0
|
||||
|
||||
tax_included = False
|
||||
with suppress(Exception):
|
||||
tax_included = bool(getattr(config, "TAX_INCLUDED", False))
|
||||
|
||||
if tax_rate <= 0:
|
||||
net = total
|
||||
else:
|
||||
if tax_included:
|
||||
divisor = 1.0 + (tax_rate / 100.0)
|
||||
if divisor <= 0:
|
||||
net = total
|
||||
else:
|
||||
net = total / divisor
|
||||
else:
|
||||
net = total
|
||||
|
||||
return round(float(net or 0.0), 2)
|
||||
|
||||
|
||||
def get_returns(period: timedelta = timedelta(days=30)) -> float:
|
||||
order_products = get_period_order_products(period, ["RETURNED"])
|
||||
|
|
@ -53,3 +70,43 @@ def get_returns(period: timedelta = timedelta(days=30)) -> float:
|
|||
|
||||
def get_total_processed_orders(period: timedelta = timedelta(days=30)) -> int:
|
||||
return get_period_order_products(period, ["RETURNED", "FINISHED"]).count()
|
||||
|
||||
|
||||
def get_daily_finished_orders_count(period: timedelta = timedelta(days=30)) -> dict[date, int]:
|
||||
current = now()
|
||||
period_start = current - period
|
||||
qs = (
|
||||
Order.objects.filter(status="FINISHED", buy_time__lte=current, buy_time__gte=period_start)
|
||||
.annotate(day=TruncDate("buy_time"))
|
||||
.values("day")
|
||||
.annotate(cnt=Count("id"))
|
||||
.order_by("day")
|
||||
)
|
||||
result: dict[date, int] = {}
|
||||
for row in qs:
|
||||
d = row.get("day")
|
||||
c = int(row.get("cnt", 0) or 0)
|
||||
if d:
|
||||
result[d] = c
|
||||
return result
|
||||
|
||||
|
||||
def get_daily_gross_revenue(period: timedelta = timedelta(days=30)) -> dict[date, float]:
|
||||
qs = (
|
||||
get_period_order_products(period, ["FINISHED"]) # OrderProduct queryset
|
||||
.annotate(day=TruncDate("order__buy_time"))
|
||||
.values("day")
|
||||
.annotate(total=Coalesce(Sum(F("buy_price") * F("quantity")), 0.0))
|
||||
.order_by("day")
|
||||
)
|
||||
result: dict[date, float] = {}
|
||||
for row in qs:
|
||||
d = row.get("day")
|
||||
total = row.get("total") or 0.0
|
||||
try:
|
||||
total_f = round(float(total), 2)
|
||||
except (TypeError, ValueError):
|
||||
total_f = 0.0
|
||||
if d:
|
||||
result[d] = total_f
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from datetime import timedelta
|
||||
import mimetypes
|
||||
import os
|
||||
import traceback
|
||||
|
|
@ -18,6 +19,7 @@ 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.views.decorators.cache import cache_page
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
|
|
@ -57,7 +59,13 @@ 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
|
||||
from engine.core.utils.commerce import (
|
||||
get_returns,
|
||||
get_revenue,
|
||||
get_total_processed_orders,
|
||||
get_daily_finished_orders_count,
|
||||
get_daily_gross_revenue,
|
||||
)
|
||||
from engine.core.utils.emailing import contact_us_email
|
||||
from engine.core.utils.languages import get_flag_by_language
|
||||
from engine.payments.serializers import TransactionProcessSerializer
|
||||
|
|
@ -489,6 +497,30 @@ 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)]
|
||||
|
||||
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]
|
||||
|
||||
context["daily_labels"] = labels
|
||||
context["daily_orders"] = orders_series
|
||||
context["daily_gross"] = gross_series
|
||||
except Exception as e: # pragma: no cover - fail safe
|
||||
logger.warning("Failed to build daily stats: %s", e)
|
||||
context["daily_labels"] = []
|
||||
context["daily_orders"] = []
|
||||
context["daily_gross"] = []
|
||||
|
||||
# Single most popular product (backward compatibility)
|
||||
most_popular: dict[str, str | int | float | None] | None = None
|
||||
# Top 10 most popular products by quantity
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ CONSTANCE_CONFIG = OrderedDict(
|
|||
("COMPANY_ADDRESS", (getenv("COMPANY_ADDRESS"), _("Address of the company"))),
|
||||
("COMPANY_PHONE_NUMBER", (getenv("COMPANY_PHONE_NUMBER"), _("Phone number of the company"))),
|
||||
("TAX_RATE", (0, _("Tax rate in jurisdiction of your company. Leave 0 if you don't want to process taxes."))),
|
||||
("TAX_INCLUDED", (True, _("Shows if the taxes are already included in product's selling prices"))),
|
||||
("EXCHANGE_RATE_API_KEY", (getenv("EXCHANGE_RATE_API_KEY", "example token"), _("Exchange rate API key"))),
|
||||
### Email Options ###
|
||||
("EMAIL_BACKEND", ("django.core.mail.backends.smtp.EmailBackend", _("!!!DO NOT CHANGE!!!"))),
|
||||
|
|
@ -58,6 +59,7 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict(
|
|||
"COMPANY_ADDRESS",
|
||||
"COMPANY_PHONE_NUMBER",
|
||||
"TAX_RATE",
|
||||
"TAX_INCLUDED",
|
||||
"EXCHANGE_RATE_API_KEY",
|
||||
),
|
||||
_("Email Options"): (
|
||||
|
|
|
|||
Loading…
Reference in a new issue