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:
parent
b837932bb4
commit
0b3ef5395f
@ -200,3 +200,19 @@ from django.contrib.messages import constants as message_constants
|
|||||||
MESSAGE_TAGS = {
|
MESSAGE_TAGS = {
|
||||||
message_constants.ERROR: 'danger',
|
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'
|
||||||
@ -40,9 +40,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# === FILTER BAR === #}
|
{# === 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">
|
<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 #}
|
{# Preserve current view mode when filtering #}
|
||||||
<input type="hidden" name="view" value="{{ view_mode }}">
|
<input type="hidden" name="view" value="{{ view_mode }}">
|
||||||
{% if view_mode == 'calendar' %}
|
{% if view_mode == 'calendar' %}
|
||||||
@ -87,16 +87,48 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Filter Button #}
|
{# Filter + Clear Buttons #}
|
||||||
<div class="col-md-3 d-flex gap-2">
|
<div class="col-md-3 d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-sm btn-accent">
|
<button type="submit" class="btn btn-sm btn-accent">
|
||||||
<i class="fas fa-filter me-1"></i> Filter
|
<i class="fas fa-filter me-1"></i> Filter
|
||||||
</button>
|
</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
|
<i class="fas fa-times me-1"></i> Clear
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -375,11 +375,25 @@ def work_history(request):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
# --- Filters ---
|
# --- 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', '')
|
worker_filter = request.GET.get('worker', '')
|
||||||
project_filter = request.GET.get('project', '')
|
project_filter = request.GET.get('project', '')
|
||||||
status_filter = request.GET.get('status', '')
|
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:
|
if worker_filter:
|
||||||
logs = logs.filter(workers__id=worker_filter).distinct()
|
logs = logs.filter(workers__id=worker_filter).distinct()
|
||||||
|
|
||||||
@ -393,6 +407,12 @@ def work_history(request):
|
|||||||
# "Unpaid" = has no PayrollRecord linked
|
# "Unpaid" = has no PayrollRecord linked
|
||||||
logs = logs.filter(payroll_records__isnull=True)
|
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)
|
# Add related data and order by date (newest first)
|
||||||
logs = logs.select_related(
|
logs = logs.select_related(
|
||||||
'project', 'supervisor'
|
'project', 'supervisor'
|
||||||
@ -435,6 +455,9 @@ def work_history(request):
|
|||||||
'is_admin': is_admin(user),
|
'is_admin': is_admin(user),
|
||||||
'view_mode': view_mode,
|
'view_mode': view_mode,
|
||||||
'filter_params': filter_params,
|
'filter_params': filter_params,
|
||||||
|
'has_active_filters': has_active_filters,
|
||||||
|
'total_log_count': total_log_count,
|
||||||
|
'filtered_log_count': filtered_log_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
# === CALENDAR MODE ===
|
# === CALENDAR MODE ===
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user