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 <noreply@anthropic.com>
This commit is contained in:
parent
22563df48a
commit
e814bc2d03
@ -367,8 +367,23 @@ button[type="submit"]:hover,
|
|||||||
.filters-main {
|
.filters-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
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 {
|
.filters-advanced {
|
||||||
@ -826,17 +841,14 @@ tr:hover {
|
|||||||
transition: width 0.3s ease-in-out;
|
transition: width 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill.low,
|
|
||||||
.progress-fill.danger {
|
.progress-fill.danger {
|
||||||
background-color: var(--color-danger-accent);
|
background-color: var(--color-danger-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill.medium,
|
|
||||||
.progress-fill.warning {
|
.progress-fill.warning {
|
||||||
background-color: var(--color-warning-accent);
|
background-color: var(--color-warning-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-fill.high,
|
|
||||||
.progress-fill.complete {
|
.progress-fill.complete {
|
||||||
background-color: var(--color-success-accent);
|
background-color: var(--color-success-accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -428,11 +428,7 @@
|
|||||||
aria-valuenow="{{ goal.percentage|unlocalize }}"
|
aria-valuenow="{{ goal.percentage|unlocalize }}"
|
||||||
aria-label="Progreso de {{ goal.name }}"
|
aria-label="Progreso de {{ goal.name }}"
|
||||||
aria-valuetext="{{ goal.progress|floatformat:1 }}€ de {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)">
|
aria-valuetext="{{ goal.progress|floatformat:1 }}€ de {{ goal.target_amount|floatformat:1 }}€ ({{ goal.percentage|floatformat:1 }}%)">
|
||||||
<div class="progress-fill
|
<div class="progress-fill {{ goal.progress_state }}"
|
||||||
{% if goal.percentage < 50 %} low
|
|
||||||
{% elif goal.percentage < 80 %} medium
|
|
||||||
{% else %} high
|
|
||||||
{% endif %}"
|
|
||||||
style="width: {{ goal.bar_width|unlocalize }}%"></div>
|
style="width: {{ goal.bar_width|unlocalize }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -9,17 +9,38 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Objetivos</h1>
|
<h1>Objetivos</h1>
|
||||||
|
|
||||||
<a class="btn btn-primary" href="{% url 'goal_create' %}">➕ Nuevo objetivo</a>
|
<div class="section-actions">
|
||||||
|
<a class="btn btn-primary" href="{% url 'goal_create' %}">➕ Nuevo objetivo</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="get" class="filters">
|
||||||
|
<div class="filters-main">
|
||||||
|
<label class="sr-only" for="filterKind">Tipo</label>
|
||||||
|
<select name="kind" id="filterKind" onchange="this.form.submit()">
|
||||||
|
<option value="">Todos los tipos</option>
|
||||||
|
{% for value, label in kind_choices %}
|
||||||
|
<option value="{{ value }}" {% if selected_kind == value %}selected{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Filtrar</button>
|
||||||
|
<a href="{% url 'goal_list' %}" class="btn btn-secondary">Limpiar</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nombre</th>
|
<th>Nombre</th>
|
||||||
<th>Tipo</th>
|
<th>Tipo</th>
|
||||||
<th>Progreso</th>
|
<th>Progreso</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% for goal in goals %}
|
{% for goal in goals %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ goal.name }}</td>
|
<td>{{ goal.name }}</td>
|
||||||
@ -53,12 +74,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td class="table-actions">
|
||||||
<a href="{% url 'goal_edit' goal.id %}">Editar</a>
|
<a href="{% url 'goal_edit' goal.id %}">Editar</a>
|
||||||
<a href="{% url 'goal_delete' goal.id %}" class="danger">Eliminar</a>
|
<a href="{% url 'goal_delete' goal.id %}" class="danger">Eliminar</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="empty-state">
|
||||||
|
<p>No hay objetivos</p>
|
||||||
|
<a href="{% url 'goal_create' %}">Añade el primero</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from django.urls import reverse
|
||||||
from expenses.models import Account, Category, Expense, Goal
|
from expenses.models import Account, Category, Expense, Goal
|
||||||
from expenses.forms import GoalForm
|
from expenses.forms import GoalForm
|
||||||
|
|
||||||
@ -194,3 +195,23 @@ def test_goalform_valid_payment_saves_goal(user, category):
|
|||||||
goal.save()
|
goal.save()
|
||||||
|
|
||||||
assert Goal.objects.filter(pk=goal.pk, name="Ahorro viaje", kind=Goal.KIND_PAYMENT, category=category).exists()
|
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"] == ""
|
||||||
|
|||||||
@ -1071,11 +1071,21 @@ def category_delete(request, pk):
|
|||||||
def goal_list(request):
|
def goal_list(request):
|
||||||
goals = Goal.objects.filter(owner=request.user)
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
"goals/list.html",
|
"goals/list.html",
|
||||||
{
|
{
|
||||||
"goals": goals,
|
"goals": goals,
|
||||||
|
"kind_choices": Goal.KIND_CHOICES,
|
||||||
|
"selected_kind": selected_kind,
|
||||||
"active_menu": "settings",
|
"active_menu": "settings",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user