Fix work history filter — add validation, explicit form action, and visual feedback

- Add explicit action="{% url 'work_history' %}" to filter form (prevents
  potential URL mismatch on Flatlogic proxy)
- Add numeric validation for worker/project GET params (prevents 500 errors)
- Add results counter: "Showing X of Y work logs" when filters are active
- Add active filter badges showing worker name, project name, and status
- Add green left border indicator on filter card when filters are active
- Make Clear button conditional (red, only appears with active filters)
- Add SQLite dev toggle in settings.py for local testing without MariaDB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-02-22 23:53:21 +02:00
parent b837932bb4
commit 0b3ef5395f
3 changed files with 77 additions and 6 deletions

View File

@ -199,4 +199,20 @@ LOGOUT_REDIRECT_URL = 'login'
from django.contrib.messages import constants as message_constants
MESSAGE_TAGS = {
message_constants.ERROR: 'danger',
}
}
# === LOCAL DEVELOPMENT: SQLite override ===
# Set USE_SQLITE=true in environment to use SQLite instead of MariaDB.
# This lets you test locally without a MySQL/MariaDB server.
if os.getenv('USE_SQLITE', 'false').lower() == 'true':
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Disable secure cookies for local http:// testing
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SAMESITE = 'Lax'

View File

@ -40,9 +40,9 @@
</div>
{# === FILTER BAR === #}
<div class="card shadow-sm border-0 mb-4">
<div class="card shadow-sm border-0 mb-4{% if has_active_filters %} border-start border-3{% endif %}" {% if has_active_filters %}style="border-left-color: var(--accent, #10b981) !important;"{% endif %}>
<div class="card-body py-3">
<form method="GET" class="row g-2 align-items-end">
<form method="GET" action="{% url 'work_history' %}" class="row g-2 align-items-end">
{# Preserve current view mode when filtering #}
<input type="hidden" name="view" value="{{ view_mode }}">
{% if view_mode == 'calendar' %}
@ -87,16 +87,48 @@
</select>
</div>
{# Filter Button #}
{# Filter + Clear Buttons #}
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-sm btn-accent">
<i class="fas fa-filter me-1"></i> Filter
</button>
<a href="{% url 'work_history' %}?view={{ view_mode }}" class="btn btn-sm btn-outline-secondary">
{% if has_active_filters %}
<a href="{% url 'work_history' %}?view={{ view_mode }}" class="btn btn-sm btn-outline-danger">
<i class="fas fa-times me-1"></i> Clear
</a>
{% endif %}
</div>
</form>
{# === Active Filter Feedback === #}
{# Shows a results counter when filters are active so the user can see the filter is working #}
{% if has_active_filters %}
<div class="mt-2 d-flex align-items-center flex-wrap gap-2">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Showing <strong>{{ filtered_log_count }}</strong> of {{ total_log_count }} work log{{ total_log_count|pluralize }}
</small>
{# Show which filters are active as small badges #}
{% if selected_worker %}
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25">
<i class="fas fa-user fa-xs me-1"></i>
{% for w in filter_workers %}{% if w.id|stringformat:"d" == selected_worker %}{{ w.name }}{% endif %}{% endfor %}
</span>
{% endif %}
{% if selected_project %}
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25">
<i class="fas fa-project-diagram fa-xs me-1"></i>
{% for p in filter_projects %}{% if p.id|stringformat:"d" == selected_project %}{{ p.name }}{% endif %}{% endfor %}
</span>
{% endif %}
{% if selected_status %}
<span class="badge bg-warning bg-opacity-10 text-dark border border-warning border-opacity-25">
<i class="fas fa-tag fa-xs me-1"></i>
{{ selected_status|capfirst }}
</span>
{% endif %}
</div>
{% endif %}
</div>
</div>

View File

@ -375,11 +375,25 @@ def work_history(request):
).distinct()
# --- Filters ---
# Read filter values from the URL query string
# Read filter values from the URL query string.
# Validate numeric params to prevent 500 errors from bad/malformed URLs.
worker_filter = request.GET.get('worker', '')
project_filter = request.GET.get('project', '')
status_filter = request.GET.get('status', '')
# Validate: worker and project must be numeric IDs (or empty)
try:
worker_filter = str(int(worker_filter)) if worker_filter else ''
except (ValueError, TypeError):
worker_filter = ''
try:
project_filter = str(int(project_filter)) if project_filter else ''
except (ValueError, TypeError):
project_filter = ''
# Count total logs BEFORE filtering (so we can show "X of Y" to the user)
total_log_count = logs.count()
if worker_filter:
logs = logs.filter(workers__id=worker_filter).distinct()
@ -393,6 +407,12 @@ def work_history(request):
# "Unpaid" = has no PayrollRecord linked
logs = logs.filter(payroll_records__isnull=True)
# Track whether any filter is active (for showing feedback in the template)
has_active_filters = bool(worker_filter or project_filter or status_filter)
# Count filtered results BEFORE adding joins (more efficient SQL)
filtered_log_count = logs.count() if has_active_filters else 0
# Add related data and order by date (newest first)
logs = logs.select_related(
'project', 'supervisor'
@ -435,6 +455,9 @@ def work_history(request):
'is_admin': is_admin(user),
'view_mode': view_mode,
'filter_params': filter_params,
'has_active_filters': has_active_filters,
'total_log_count': total_log_count,
'filtered_log_count': filtered_log_count,
}
# === CALENDAR MODE ===