Añadidas tandas: 4, 4b y 7 #33
@ -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);
|
||||
}
|
||||
|
||||
@ -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 }}%)">
|
||||
<div class="progress-fill
|
||||
{% if goal.percentage < 50 %} low
|
||||
{% elif goal.percentage < 80 %} medium
|
||||
{% else %} high
|
||||
{% endif %}"
|
||||
<div class="progress-fill {{ goal.progress_state }}"
|
||||
style="width: {{ goal.bar_width|unlocalize }}%"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -9,17 +9,38 @@
|
||||
{% block content %}
|
||||
<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">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Tipo</th>
|
||||
<th>Progreso</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for goal in goals %}
|
||||
<tr>
|
||||
<td>{{ goal.name }}</td>
|
||||
@ -53,12 +74,20 @@
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<td class="table-actions">
|
||||
<a href="{% url 'goal_edit' goal.id %}">Editar</a>
|
||||
<a href="{% url 'goal_delete' goal.id %}" class="danger">Eliminar</a>
|
||||
</td>
|
||||
</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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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"] == ""
|
||||
|
||||
@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user