Report: filter-pill strip with × to clear individual filters

Three pills under the header: date range, project(s), team(s). Shows
comma-joined names when multi-valued (project_name in context is
already a comma-joined string from Task 6). × buttons on the project
and team pills remove just that filter via a rebuilt querystring;
the calendar pill has no × (date range is required).

Helper context keys query_string_without_project / _without_team do
the rebuild in the view via QueryDict.setlist so multi-value keys
are properly stripped (pop() only removes the first occurrence).

Pill CSS uses existing design tokens (--bg-inset, --accent,
--text-primary, --border-default, --text-tertiary, --bg-card-hover)
so dark and light themes work without overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-04-22 23:14:48 +02:00
parent 702bba10ed
commit ea481bfbf4
3 changed files with 61 additions and 0 deletions

View File

@ -30,6 +30,25 @@
</div>
</div>
{# === FILTER PILLS === #}
<div class="filter-pill-strip d-flex flex-wrap gap-2 mb-4 d-print-none">
<span class="filter-pill">
<i class="fas fa-calendar me-1"></i>{{ start_date|date:"d M Y" }} &ndash; {{ end_date|date:"d M Y" }}
</span>
<span class="filter-pill">
<i class="fas fa-folder me-1"></i>{{ project_name }}
{% if selected_project_ids %}
<a href="?{{ query_string_without_project|default:query_string }}" class="filter-pill__x" aria-label="Clear project filter">&times;</a>
{% endif %}
</span>
<span class="filter-pill">
<i class="fas fa-users me-1"></i>{{ team_name }}
{% if selected_team_ids %}
<a href="?{{ query_string_without_team|default:query_string }}" class="filter-pill__x" aria-label="Clear team filter">&times;</a>
{% endif %}
</span>
</div>
<!-- === PRINT HEADER === -->
<div class="d-none d-print-block mb-4">
<h2 class="text-center fw-bold mb-1">FoxFitt Construction &mdash; Payroll Report</h2>

View File

@ -2382,6 +2382,17 @@ def generate_report(request):
)
# Pass the raw query params so the "Download PDF" button can use them
context['query_string'] = request.GET.urlencode()
# === FILTER PILL CLEAR LINKS ===
# For the filter-pill x buttons: rebuild the querystring with one filter removed.
# QueryDict.pop() only removes the first occurrence, so for multi-value keys
# (e.g. project=1&project=2) we follow up with setlist(key, []) to strip them all.
def _qs_without(key):
qd = request.GET.copy()
qd.pop(key, None)
qd.setlist(key, [])
return qd.urlencode()
context['query_string_without_project'] = _qs_without('project')
context['query_string_without_team'] = _qs_without('team')
# Pass projects and teams so the "New Report" modal's dropdowns can
# populate (same lists the Dashboard modal uses)
context['projects'] = Project.objects.all().order_by('name')

View File

@ -1491,3 +1491,34 @@ body, .card, .modal-content, .form-control, .form-select,
.work-log-row:hover td {
background: var(--bg-card-hover);
}
/* === Report filter pills === */
.filter-pill {
display: inline-flex;
align-items: center;
padding: 0.35rem 0.75rem;
font-size: 0.825rem;
background: var(--bg-inset);
color: var(--text-primary);
border: 1px solid var(--border-default);
border-radius: 999px;
line-height: 1.2;
}
.filter-pill i {
color: var(--accent);
font-size: 0.75rem;
}
.filter-pill__x {
margin-left: 0.5rem;
padding: 0 0.35rem;
color: var(--text-tertiary);
text-decoration: none;
font-weight: 600;
border-radius: 50%;
transition: color 120ms, background-color 120ms;
}
.filter-pill__x:hover {
color: var(--text-primary);
background: var(--bg-card-hover);
text-decoration: none;
}