Some improvements and fixes in dashboard

This commit is contained in:
JKuijperM 2026-06-11 11:15:52 +02:00
parent 6c51b5a9b2
commit 2c2ef350b0
3 changed files with 300 additions and 203 deletions

View File

@ -99,9 +99,18 @@ a.danger {
position: relative;
}
.charts-container {
display: flex;
gap: 2rem;
align-items: flex-start;
flex-wrap: wrap;
justify-content: flex-start;
margin-bottom: 4rem;
}
.chart-box {
height: 300px;
margin-bottom: 2rem;
margin-bottom: 3rem;
}
.canvas-wrapper {
@ -119,9 +128,9 @@ a.danger {
.kpi-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
margin: 2rem 0;
}
.kpi-card {
@ -143,6 +152,13 @@ a.danger {
margin-top: 0.25rem;
}
.comparison {
background: #f9f9f9;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 8px;
}
/* .comparison strong {
color:#b91c1c;
} */

View File

@ -30,66 +30,78 @@
<section class="dashboard-filters">
<div class="dashboard-presets">
<a href="{% url 'dashboard' %}" data-period="this_month" class="preset {% if period == 'this_month' %}active{% endif %}">
<a href="{% url 'dashboard' %}?period=this_month" class="preset {% if period == 'this_month' %}active{% endif %}">
Este mes
</a>
<a href="{% url 'dashboard' %}" data-period="last_month" class="preset {% if period == 'last_month' %}active{% endif %}">
<a href="{% url 'dashboard' %}?period=last_month" class="preset {% if period == 'last_month' %}active{% endif %}">
Mes anterior
</a>
<a href="{% url 'dashboard' %}" data-period="this_year" class="preset {% if period == 'this_year' %}active{% endif %}">
<a href="{% url 'dashboard' %}?period=this_year" class="preset {% if period == 'this_year' %}active{% endif %}">
Este año
</a>
{% if period %}
<a href="{% url 'dashboard' %}" style="color: red; margin-left: 10px;">❌ Quitar filtros temporales</a>
{% endif %}
</div>
<br>
<form method="get">
<form method="get" action="{% url 'dashboard' %}" id="filtered_form">
{% if period %}
<input type="hidden" name="period" value="{{ period }}" id="hiddenPeriod">
{% endif %}
<select name="account" id="account">
<option value="">Todas las cuentas</option>
{% for acc in accounts %}
<option value="{{ acc.id }}"
{% if selected_account == acc.id %}selected{% endif %}
>
<option value="{{ acc.id }}" {% if selected_account == acc.id %}selected{% endif %}>
{{ acc.name }}
</option>
{% endfor %}
</select>
<select name="year">
{% for y in year_list %}
<option value="{{ y }}"
{% if y == selected_year %}selected{% endif %}
>
{{ y }}
</option>
{% endfor %}
</select>
{% if not period %}
<select name="year" id="yearSelect">
{% for y in year_list %}
<option value="{{ y }}" {% if y == selected_year %}selected{% endif %}>
{{ y }}
</option>
{% endfor %}
</select>
<select name="month">
<option value="">Todo el año</option>
{% for m in months %}
<option value="{{ m }}"
{% if m == selected_month %}selected{% endif %}
>
{{ m }}
</option>
{% endfor %}
</select>
<select name="month" id="monthSelect">
<option value="">Todo el año</option>
{% for m in months %}
<option value="{{ m }}"{% if m == selected_month %}selected{% endif %}>
{{ m }}
</option>
{% endfor %}
</select>
{% endif %}
<label class="checkbox">
<input type="checkbox" name="compare" value="1"
{% if compare_enabled %}checked{% endif %}>
Comparar
<input type="checkbox" name="compare" value="1" {% if compare_enabled %}checked{% endif %}>
Comparar periodo anterior
</label>
<button type="submit">Aplicar</button>
<button type="submit">Aplicar Filtros</button>
</form>
</section>
<script>
const yearSelect = document.getElementById('yearSelect');
const monthSelect = document.getElementById('monthSelect');
const hiddenPeriod = document.getElementById('hiddenPeriod');
function clearPeriodPreset() {
if (hiddenPeriod) {
hiddenPeriod.value = ""
}
document.querySelectorAll('.preset').forEach(btn => btn.classList.remove('active'));
}
</script>
<!-- <script>
document.querySelectorAll('.dashboard-presets a').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
@ -104,14 +116,13 @@
}
});
});
</script>
</script> -->
<!-- ========================= -->
<!-- KPIs -->
<!-- ========================= -->
<section class="kpi-grid">
<div class="kpi-card">
<span class="kpi-label">Saldo actual</span>
<span class="kpi-value">{{ kpi_balance|floatformat:2 }}€</span>
@ -131,25 +142,78 @@
<span class="kpi-label">Categorías</span>
<span class="kpi-value">{{ kpi_categories }}</span>
</div>
</section>
{% if compare_enabled %}
<section class="comparison">
<h3>📊 Resumen comparativo</h3>
<p>
Gastos periodo actual: <strong>{{ kpi_total|floatformat:2 }} €</strong><br>
Gastos periodo anterior: <strong>{{ kpi_previous_total|floatformat:2 }} €</strong><br>
Diferencia:
<strong style="color: {% if kpi_trend == 'up' %}#d9534f{% else %}#5cb85c{% endif %};">
{% if kpi_trend == "up" %}+{% endif %}{{ kpi_difference|floatformat:2 }}€ ({{ kpi_percentage|floatformat:1 }}%)
</strong>
<small style="display:block; color:gray;">
{% if kpi_trend == "up" %}⚠️ Has gastado más que el periodo pasado. {% else %} ¡Bien! Has reducido tus gastos. {% endif %}
</small>
</p>
</section>
{% endif %}
<!-- ========================= -->
<!-- Charts -->
<!-- ========================= -->
<section class="chart-box">
<canvas id="mainChart"></canvas>
</section>
<div class="charts-container">
<section class="chart-box" style="width: 45%; min-width: 300px; max-width: 550px;">
<h3>Evolución de Gastos ({% if chart_type == 'day' %}Por día {% else %} Por Meses{% endif %})</h3>
<div style="position: relative; width: 100%; height: 350px; padding-bottom: 20px;">
<canvas id="mainChart"></canvas>
</div>
</section>
<section class="chart-box" style="width: 30%; min-width: 280px; max-width: 400px;">
<h3>Distribución por Categorías</h3>
<div style="position: relative; width: 100%; height: 350px; display: flex; align-items: center; justify-content: center">
<canvas id="categoryChart"></canvas>
</div>
</section>
</div>
<div style="clear: both; margin-top: 2rem;"></div>
<script>
const catLabels = [{% for row in by_category_chart %}"{{ row.category__name }}",{% endfor %}];
const catData = [{% for row in by_category_chart %}{{ row.total|unlocalize }},{% endfor %}];
new Chart(document.getElementById('categoryChart'), {
type: 'pie',
data: {
labels: catLabels,
datasets: [{
data: catData,
backgroundColor: [
'#ff6384', '#36a2eb', '#cc65fe', '#ffce56', '#4bc0c0', '#ffa1b5'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'right' }
}
}
});
</script>
<h3>Evolución anual por cuenta ({{ selected_year }})</h3>
<div class="dashboard-grid">
{% for acc in accounts_charts %}
<div class="card card-chart">
<h3>{{ acc.name }}</h3>
<p><strong>Saldo actual:</strong> {{ acc.current_balance|floatformat:2 }} €</p>
<h4>{{ acc.name }}</h4>
<p><strong>Saldo actual:</strong> {{ acc.current_balance|floatformat:2 }}€</p>
<div class="canvas-wrapper">
<canvas id="accountChart{{ acc.id }}"></canvas>
</div>
@ -160,7 +224,6 @@
<script>
const labels = {{ chart_labels|safe }};
const data = {{ chart_data|safe }};
const ctx = document.getElementById('mainChart');
new Chart(ctx, {
@ -168,35 +231,54 @@
data: {
labels: labels,
datasets:[{
label: 'Gastos',
label: 'Gastos (€)',
data: data,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
backgroundColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } }
}
});
</script>
<script>
const monthLabels = {{ chart_labels|safe }};
const allMonths = ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"];
{% for acc in accounts_charts %}
new Chart(
document.getElementById('accountChart{{ acc.id }}'),
{
type: 'line',
data: {
labels: monthLabels,
datasets: [{
label: 'Saldo',
data: {{ acc.data|safe }},
fill: true,
tension: 0.3,
}]
},
options: {
responsive: true,
maintainAspectRatio: false
(function() {
const dataAccount = {{ acc.data|safe }};
const filteredLabels = allMonths.slice(0, dataAccount.length);
new Chart(
document.getElementById('accountChart{{ acc.id }}'),
{
type: 'line',
data: {
labels: allMonths,
datasets: [{
label: 'Saldo Real (€)',
data: dataAccount,
fill: false,
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false
}
}
}
}
}
);
);
})();
{% endfor %}
</script>

View File

@ -19,7 +19,7 @@ from django.db.models import Sum
from django.contrib.auth import login
from django.core.paginator import Paginator
from django.utils.ipv6 import is_valid_ipv6_address
from django.db.models.functions import ExtractMonth, ExtractYear
from django.db.models.functions import ExtractMonth, ExtractYear, ExtractDay
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, render, redirect
@ -287,177 +287,176 @@ def expense_delete(request, pk):
@login_required
def dashboard(request):
# ------------------
# Filters
# ------------------
year = _get_int(request.GET.get("year"))
month = _get_int(request.GET.get("month"))
period = request.GET.get("period")
today = date.today()
period = request.GET.get("period", "")
account_id = _get_int(request.GET.get("account"))
compare_enabled = request.GET.get("compare") == "1"
today = date.today()
current_year = today.year
accounts = Account.objects.filter(owner=request.user, active=True)
selected_year = year or current_year
selected_month = month
# ------------------
# Queryset base
# -----------------
expenses = Expense.objects.filter(owner=request.user)
selected_account_obj = None
if account_id:
expenses = expenses.filter(account_id=account_id)
selected_account_obj = accounts.filter(id=account_id).first()
# Time presets
if period == "this_month":
selected_year, selected_month = today.year, today.month
elif period == "last_month":
selected_year, selected_month = sub_months(today.year, today.month, 1)
elif period == "this_year":
selected_year, selected_month = today.year, None
else:
selected_year = _get_int(request.GET.get("year")) or _get_int(today.year)
selected_month = _get_int(request.GET.get("month"))
selected_month = _get_int(selected_month) if selected_month else None
# Accounts
accounts = Account.objects.filter(owner=request.user, active=True)
selected_account_obj = None
if account_id:
selected_account_obj = accounts.filter(id=account_id).first()
# Calculate the KPI
if selected_account_obj:
kpi_balance = selected_account_obj.current_balance()
else:
kpi_balance = sum(account.current_balance() for account in accounts)
# Filter by base expenses
expenses = Expense.objects.filter(owner=request.user)
if account_id:
expenses = expenses.filter(account_id=account_id)
expenses_filtered = expenses.filter(date__year=selected_year)
if selected_month:
expenses_filtered = expenses_filtered.filter(date__month=selected_month)
# Basic KPIs
total_amount = expenses_filtered.aggregate(total=Sum("amount"))["total"] or 0
expense_count = expenses_filtered.count()
category_count = expenses_filtered.values("category").distinct().count()
# ------------------
# Totals by category
# -----------------
by_category = (
# Total by category
by_category_qs = by_category = (
expenses_filtered.values("category__name")
.annotate(total=Sum("amount"))
.order_by("category__name")
.order_by("-total")
)
# ------------------
# Totals by month
# -----------------
by_month_qs = (
expenses_filtered.annotate(month=ExtractMonth("date"))
.values("month")
.annotate(total=Sum("amount"))
)
month_totals = {row["month"]: float(row["total"]) for row in by_month_qs}
months = list(range(1, 13))
by_month = [{"month": m, "total": month_totals.get(m, 0)}for m in months]
# ------------------
# Availables years
# -----------------
CATEGORY_LIMIT = 10 # Top 10
by_category_chart = []
for index, row in enumerate(by_category_qs):
if index >= CATEGORY_LIMIT:
break
cat_name = row["category__name"] or "Sin categoría"
total_cat = float(row["total"])
by_category_chart.append({
"category__name": cat_name,
"total": total_cat
})
#Graphic
if selected_month:
by_day_qs = (
expenses_filtered.annotate(day=ExtractDay("date"))
.values("day")
.annotate(total=Sum("amount"))
)
day_totals = {row["day"]: float(row["total"]) for row in by_day_qs}
import calendar
num_days = calendar.monthrange(selected_year, selected_month)[1]
chart_labels = list(range(1, num_days + 1))
chart_totals = [day_totals.get(d, 0) for d in chart_labels]
chart_type = "day"
else:
by_month_qs = (
expenses_filtered.annotate(month=ExtractMonth("date"))
.values("month")
.annotate(total=Sum("amount"))
)
month_totals = {row["month"]: float(row["total"]) for row in by_month_qs}
chart_labels = ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"]
chart_totals = [month_totals.get(m, 0) for m in range(1, 13)]
chart_type = "month"
year_list = (
expenses.annotate(year=ExtractYear("date"))
Expense.objects.filter(owner=request.user)
.annotate(year=ExtractYear("date"))
.values_list("year", flat=True)
.distinct()
.order_by("year")
.order_by("-year")
)
# ------------------
# Chart
# -----------------
chart_labels = months
chart_totals = [row["total"] for row in by_month]
# ------------------
# Compare period
# -----------------
previous_total = None
kpi_difference = None
percentage = None
category_comparison = None
kpi_trend = None
kpi_difference_abs = None
# Comparison
previous_total = 0
kpi_difference = 0
percentage = 0
kpi_trend = "equal"
category_comparison = []
if compare_enabled:
previous_expenses = Expense.objects.filter(owner=request.user)
if account_id:
previous_expenses = previous_expenses.filter(account_id=account_id)
if selected_month:
prev_year, prev_month = sub_months(selected_year, selected_month, 1)
previous_expenses = previous_expenses.filter(
date__year=prev_year, date__month=prev_month
)
previous_expenses = previous_expenses.filter(date__year=prev_year, date__month=prev_month)
else:
# Anual compare
prev_year = selected_year - 1
previous_expenses = previous_expenses.filter(date__year=prev_year)
previous_total = previous_expenses.aggregate(total=Sum("amount"))["total"] or 0
kpi_difference = total_amount - previous_total
if previous_total:
if previous_total > 0:
percentage = (kpi_difference / previous_total) * 100
if kpi_difference is not None:
if kpi_difference > 0:
kpi_trend = "up"
elif kpi_difference < 0:
kpi_trend = "down"
else:
kpi_trend = "equal"
kpi_difference_abs = abs(kpi_difference) if kpi_difference is not None else None
# ------------------
# Previous expenses by category
# ------------------
if kpi_difference > 0:
kpi_trend = "up"
elif kpi_difference < 0:
kpi_trend = "down"
# Comparison by category
previous_by_category = previous_expenses.values("category__name").annotate(total=Sum("amount"))
current_map = {row["category__name"]: row["total"] for row in by_category}
previous_map = {row["category__name"]: row["total"] for row in previous_by_category}
all_categories = set(current_map) | set(previous_map)
category_comparison = []
for category in all_categories:
current_total = current_map.get(category, 0)
previous_total_cat = previous_map.get(category, 0)
difference = current_total - previous_total_cat
category_comparison.append(
{
"category": category,
"current": current_total,
"previous": previous_total_cat,
"difference": difference,
"difference_abs": abs(difference),
}
)
# ------------------
# Previous expenses by category
# ------------------
current_map = {row["category__name"]: float(row["total"]) for row in by_category}
previous_map = {row["category__name"]: float(row["total"]) for row in previous_by_category}
for cat in set(current_map) | set(previous_map):
cur_t = current_map.get(cat, 0)
prev_t = previous_map.get(cat, 0)
category_comparison.append({
"category": cat,
"current": cur_t,
"previous": prev_t,
"difference": cur_t - prev_t,
"difference_abs": abs(cur_t - prev_t)
})
# Anual evolution by accounts
accounts_charts = []
for account in accounts:
monthly_data = account.monthly_balance(selected_year)
m_balance = [row["balance"] for row in monthly_data]
accounts_charts.append(
{
"id": account.id,
"name": account.name,
"data": m_balance,
"current_balance": account.current_balance(),
}
)
for acc in accounts:
try:
monthly_data = acc.monthly_balance(selected_year)
m_balance = [float(row["balance"]) for row in monthly_data]
except:
m_balance = [0] * 12
if selected_year == today.year:
current_month_index = today.month - 1
if current_month_index < len(m_balance):
m_balance[current_month_index] = float(acc.current_balance())
m_balance = m_balance[:today.month]
accounts_charts.append({
"id": acc.id,
"name": acc.name,
"data": m_balance,
"current_balance": acc.current_balance(),
})
# Goals
goals = Goal.objects.filter(owner=request.user)
@ -468,11 +467,11 @@ def dashboard(request):
{
"active_menu": "dashboard",
"by_category": by_category,
"by_month": by_month,
"by_category_chart": by_category_chart,
"chart_labels": chart_labels,
"chart_data": chart_totals,
"year_list": year_list,
"months": months,
"months": list(range(1, 13)),
"selected_year": selected_year,
"selected_month": selected_month,
"kpi_total": total_amount,
@ -481,7 +480,7 @@ def dashboard(request):
"compare_enabled": compare_enabled,
"kpi_previous_total": previous_total,
"kpi_difference": kpi_difference,
"kpi_difference_abs": kpi_difference_abs,
"kpi_difference_abs": abs(kpi_difference),
"kpi_percentage": percentage,
"category_comparison": category_comparison,
"kpi_trend": kpi_trend,