From e814bc2d030326533653d6868e6d839773497698 Mon Sep 17 00:00:00 2001 From: JKuijperM Date: Wed, 9 Sep 2026 16:16:58 +0200 Subject: [PATCH] Anade filtro por tipo en objetivos y unifica la barra de progreso El listado de objetivos mezclaba pagos, presupuestos y ahorros sin forma de separarlos. Anade un filtro por tipo, con el mismo patron que los filtros de gastos, y ordena los objetivos por nombre. Un valor de "kind" que no este en KIND_CHOICES se ignora en vez de romper. De paso, la barra de progreso pasa a usar goal.progress_state en lugar de decidir el color en la plantilla con un if sobre el porcentaje. La logica ya vivia en el modelo, que ademas distingue presupuestos (donde acercarse al 100% es un aviso) del resto; la plantilla del dashboard seguia con las clases antiguas low/medium/high, que se retiran del CSS. Incluye estado vacio y thead/tbody en la tabla, que le faltaban. Co-Authored-By: Claude Opus 5 --- .../expenses/static/expenses/css/base.css | 20 ++- .../templates/expenses/dashboard.html | 6 +- .../expenses/templates/goals/list.html | 121 +++++++++++------- expenses_manager/expenses/tests/test_goals.py | 21 +++ expenses_manager/expenses/views.py | 10 ++ 5 files changed, 123 insertions(+), 55 deletions(-) diff --git a/expenses_manager/expenses/static/expenses/css/base.css b/expenses_manager/expenses/static/expenses/css/base.css index 48588b1..fb214c5 100644 --- a/expenses_manager/expenses/static/expenses/css/base.css +++ b/expenses_manager/expenses/static/expenses/css/base.css @@ -367,8 +367,23 @@ button[type="submit"]:hover, .filters-main { display: flex; flex-wrap: wrap; - gap: 0.5rem; align-items: center; + gap: 0.5rem; +} + +.filters-main select { + padding: 0.5rem 0.7rem; + font-family: inherit; + font-size: 0.95rem; + color: var(--color-text); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 6px; +} + +.filters-main select:focus { + outline: 2px solid var(--color-focus-ring); + outline-offset: 1px; } .filters-advanced { @@ -826,17 +841,14 @@ tr:hover { transition: width 0.3s ease-in-out; } -.progress-fill.low, .progress-fill.danger { background-color: var(--color-danger-accent); } -.progress-fill.medium, .progress-fill.warning { background-color: var(--color-warning-accent); } -.progress-fill.high, .progress-fill.complete { background-color: var(--color-success-accent); } diff --git a/expenses_manager/expenses/templates/expenses/dashboard.html b/expenses_manager/expenses/templates/expenses/dashboard.html index 49df5a5..f9ff28b 100644 --- a/expenses_manager/expenses/templates/expenses/dashboard.html +++ b/expenses_manager/expenses/templates/expenses/dashboard.html @@ -428,11 +428,7 @@ aria-valuenow="{{ goal.percentage|unlocalize }}" aria-label="Progreso de {{ goal.name }}" aria-valuetext="{{ goal.progress|floatformat:1 }}€ de {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)"> -
diff --git a/expenses_manager/expenses/templates/goals/list.html b/expenses_manager/expenses/templates/goals/list.html index 9806361..66a4ee8 100644 --- a/expenses_manager/expenses/templates/goals/list.html +++ b/expenses_manager/expenses/templates/goals/list.html @@ -9,57 +9,86 @@ {% block content %}

Objetivos

-➕ Nuevo objetivo + + +
+
+ + + + + Limpiar +
+
- - - - - - - - {% for goal in goals %} - - - - + + + + + + + + + {% for goal in goals %} + + + + + - - - {% endfor %} + + + {% empty %} + + + + {% endfor %} +
NombreTipoProgreso
{{ goal.name }} - {{ goal.get_kind_display }} - {% if goal.kind == "budget" %} - ({{ goal.get_period_display|lower }}) - {% endif %} - -
- - {{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€ - -
-
-
- - {{ goal.percentage|floatformat:1 }}% - - {% if goal.is_exceeded %} - Excedido +
NombreTipoProgreso
{{ goal.name }} + {{ goal.get_kind_display }} + {% if goal.kind == "budget" %} + ({{ goal.get_period_display|lower }}) {% endif %} - - +
+ + {{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€ + +
+
+
+ + {{ goal.percentage|floatformat:1 }}% + + {% if goal.is_exceeded %} + Excedido + {% endif %} +
+
- Editar - Eliminar -
+ Editar + Eliminar +
+

No hay objetivos

+ Añade el primero +
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/expenses_manager/expenses/tests/test_goals.py b/expenses_manager/expenses/tests/test_goals.py index e7717ae..2b3066e 100644 --- a/expenses_manager/expenses/tests/test_goals.py +++ b/expenses_manager/expenses/tests/test_goals.py @@ -1,6 +1,7 @@ import pytest from datetime import date from decimal import Decimal +from django.urls import reverse from expenses.models import Account, Category, Expense, Goal from expenses.forms import GoalForm @@ -194,3 +195,23 @@ def test_goalform_valid_payment_saves_goal(user, category): goal.save() assert Goal.objects.filter(pk=goal.pk, name="Ahorro viaje", kind=Goal.KIND_PAYMENT, category=category).exists() + + +def test_goal_list_filters_by_kind(auth_client, user, category): + Goal.objects.create(owner=user, name="Pago", target_amount=Decimal("100"), kind=Goal.KIND_PAYMENT, category=category) + Goal.objects.create(owner=user, name="Presupuesto", target_amount=Decimal("200"), kind=Goal.KIND_BUDGET, category=category, period=Goal.PERIOD_MONTH) + + response = auth_client.get(reverse('goal_list'), {"kind": Goal.KIND_BUDGET}) + + assert [g.name for g in response.context["goals"]] == ["Presupuesto"] + assert response.context["selected_kind"] == Goal.KIND_BUDGET + + +def test_goal_list_ignores_invalid_kind_param(auth_client, user, category): + Goal.objects.create(owner=user, name="Pago", target_amount=Decimal("100"), kind=Goal.KIND_PAYMENT, category=category) + + response = auth_client.get(reverse('goal_list'), {"kind": "bogus"}) + + assert response.status_code == 200 + assert [g.name for g in response.context["goals"]] == ["Pago"] + assert response.context["selected_kind"] == "" diff --git a/expenses_manager/expenses/views.py b/expenses_manager/expenses/views.py index a24ffc9..906a9a1 100644 --- a/expenses_manager/expenses/views.py +++ b/expenses_manager/expenses/views.py @@ -1071,11 +1071,21 @@ def category_delete(request, pk): def goal_list(request): goals = Goal.objects.filter(owner=request.user) + selected_kind = request.GET.get("kind") or "" + if selected_kind in dict(Goal.KIND_CHOICES): + goals = goals.filter(kind=selected_kind) + else: + selected_kind = "" + + goals = goals.order_by("name") + return render( request, "goals/list.html", { "goals": goals, + "kind_choices": Goal.KIND_CHOICES, + "selected_kind": selected_kind, "active_menu": "settings", }, )