Añadidas tandas: 4, 4b y 7 #33

Merged
jkuijperm merged 13 commits from dev into main 2026-09-09 14:29:29 +00:00
5 changed files with 123 additions and 55 deletions
Showing only changes of commit e814bc2d03 - Show all commits

View File

@ -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);
} }

View File

@ -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>

View File

@ -9,57 +9,86 @@
{% 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>
<tr> <thead>
<th>Nombre</th> <tr>
<th>Tipo</th> <th>Nombre</th>
<th>Progreso</th> <th>Tipo</th>
<th></th> <th>Progreso</th>
</tr> <th></th>
</tr>
{% for goal in goals %} </thead>
<tr> <tbody>
<td>{{ goal.name }}</td> {% for goal in goals %}
<td> <tr>
{{ goal.get_kind_display }} <td>{{ goal.name }}</td>
{% if goal.kind == "budget" %} <td>
<small>({{ goal.get_period_display|lower }})</small> {{ goal.get_kind_display }}
{% endif %} {% if goal.kind == "budget" %}
</td> <small>({{ goal.get_period_display|lower }})</small>
<td>
<div class="progress-container">
<span class="progress-label">
{{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€
</span>
<div class="progress-bar"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
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 }}%)">
<div class="progress-fill {{ goal.progress_state }}"
style="width: {{ goal.bar_width|unlocalize }}%"></div>
</div>
<span class="progress-label">
{{ goal.percentage|floatformat:1 }}%
</span>
{% if goal.is_exceeded %}
<span class="badge badge-inactive">Excedido</span>
{% endif %} {% endif %}
</div> </td>
</td> <td>
<div class="progress-container">
<span class="progress-label">
{{ goal.progress|floatformat:1 }}€ / {{ goal.target_amount|floatformat:1 }}€
</span>
<div class="progress-bar"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
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 }}%)">
<div class="progress-fill {{ goal.progress_state }}"
style="width: {{ goal.bar_width|unlocalize }}%"></div>
</div>
<span class="progress-label">
{{ goal.percentage|floatformat:1 }}%
</span>
{% if goal.is_exceeded %}
<span class="badge badge-inactive">Excedido</span>
{% endif %}
</div>
</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>
{% endfor %} {% 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 %}
</tbody>
</table> </table>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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"] == ""

View File

@ -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",
}, },
) )