Añadidas tandas: 4, 4b y 7 #33
@ -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