This commit is contained in:
Flatlogic Bot 2026-02-08 22:15:25 +00:00
parent 8de72675e5
commit fbcc2964bf
6 changed files with 148 additions and 7 deletions

View File

@ -12,6 +12,9 @@
<button type="button" class="btn btn-outline-primary shadow-sm py-1 px-4 flex-grow-1" data-bs-toggle="modal" data-bs-target="#mapModal"> <button type="button" class="btn btn-outline-primary shadow-sm py-1 px-4 flex-grow-1" data-bs-toggle="modal" data-bs-target="#mapModal">
<i class="bi bi-map-fill me-1"></i> View Map <i class="bi bi-map-fill me-1"></i> View Map
</button> </button>
<a href="{% url 'neighborhood_counts' %}?city={{ city_filter|urlencode }}&district={{ district_filter|urlencode }}&neighborhood={{ neighborhood_filter|urlencode }}&address={{ address_filter|urlencode }}" class="btn btn-outline-info shadow-sm py-1 px-4 flex-grow-1">
<i class="bi bi-grid-3x3-gap-fill me-1"></i> Neighborhoods
</a>
<a href="{% url 'door_visit_history' %}" class="btn btn-outline-success shadow-sm py-1 px-4 flex-grow-1"> <a href="{% url 'door_visit_history' %}" class="btn btn-outline-success shadow-sm py-1 px-4 flex-grow-1">
<i class="bi bi-clock-history me-1"></i> Visit History <i class="bi bi-clock-history me-1"></i> Visit History
</a> </a>
@ -25,14 +28,18 @@
</div> </div>
<div class="card-body p-3 p-md-4"> <div class="card-body p-3 p-md-4">
<form method="GET" action="." class="row g-3 align-items-end"> <form method="GET" action="." class="row g-3 align-items-end">
<div class="col-12 col-md-3"> <div class="col-12 col-md-2">
<label class="form-label small fw-bold text-uppercase text-muted">District</label> <label class="form-label small fw-bold text-uppercase text-muted">District</label>
<input type="text" name="district" class="form-control rounded-3" placeholder="Filter by district..." value="{{ district_filter }}"> <input type="text" name="district" class="form-control rounded-3" placeholder="Filter by district..." value="{{ district_filter }}">
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-2">
<label class="form-label small fw-bold text-uppercase text-muted">Neighborhood</label> <label class="form-label small fw-bold text-uppercase text-muted">Neighborhood</label>
<input type="text" name="neighborhood" class="form-control rounded-3" placeholder="Filter by neighborhood..." value="{{ neighborhood_filter }}"> <input type="text" name="neighborhood" class="form-control rounded-3" placeholder="Filter by neighborhood..." value="{{ neighborhood_filter }}">
</div> </div>
<div class="col-12 col-md-2">
<label class="form-label small fw-bold text-uppercase text-muted">City</label>
<input type="text" name="city" class="form-control rounded-3" placeholder="Filter by city..." value="{{ city_filter }}">
</div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<label class="form-label small fw-bold text-uppercase text-muted">Address Search</label> <label class="form-label small fw-bold text-uppercase text-muted">Address Search</label>
<input type="text" name="address" class="form-control rounded-3" placeholder="Filter by street address..." value="{{ address_filter }}"> <input type="text" name="address" class="form-control rounded-3" placeholder="Filter by street address..." value="{{ address_filter }}">
@ -130,12 +137,12 @@
<ul class="pagination justify-content-center mb-0"> <ul class="pagination justify-content-center mb-0">
{% if households.has_previous %} {% if households.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page=1{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="First"> <a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page=1{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="First">
<i class="bi bi-chevron-double-left small"></i> <i class="bi bi-chevron-double-left small"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.previous_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Previous"> <a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.previous_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Previous">
<i class="bi bi-chevron-left small"></i> <i class="bi bi-chevron-left small"></i>
</a> </a>
</li> </li>
@ -145,12 +152,12 @@
{% if households.has_next %} {% if households.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.next_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Next"> <a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.next_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Next">
<i class="bi bi-chevron-right small"></i> <i class="bi bi-chevron-right small"></i>
</a> </a>
</li> </li>
<li class="page-item"> <li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.paginator.num_pages }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Last"> <a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.paginator.num_pages }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Last">
<i class="bi bi-chevron-double-right small"></i> <i class="bi bi-chevron-double-right small"></i>
</a> </a>
</li> </li>

View File

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container-fluid py-4 py-md-5 px-3 px-md-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 mb-md-5 gap-3">
<div>
<h1 class="h2 fw-bold text-dark mb-1">Neighborhood Summary</h1>
<p class="text-muted mb-0">Household counts by neighborhood based on active filters.</p>
</div>
<div>
<a href="{% url 'door_visits' %}?city={{ city_filter|urlencode }}&district={{ district_filter|urlencode }}&neighborhood={{ neighborhood_filter|urlencode }}&address={{ address_filter|urlencode }}" class="btn btn-outline-secondary shadow-sm py-1 px-4">
<i class="bi bi-arrow-left me-1"></i> Back to Door Visits
</a>
</div>
</div>
<!-- Filters Info -->
{% if district_filter or neighborhood_filter or city_filter or address_filter %}
<div class="alert alert-info border-0 shadow-sm rounded-4 mb-4">
<i class="bi bi-info-circle-fill me-2"></i>
Active Filters:
{% if district_filter %}<strong>District:</strong> {{ district_filter }}{% endif %}
{% if neighborhood_filter %}{% if district_filter %}, {% endif %}<strong>Neighborhood:</strong> {{ neighborhood_filter }}{% endif %}
{% if city_filter %}{% if district_filter or neighborhood_filter %}, {% endif %}<strong>City:</strong> {{ city_filter }}{% endif %}
{% if address_filter %}{% if district_filter or neighborhood_filter or city_filter %}, {% endif %}<strong>Address:</strong> {{ address_filter }}{% endif %}
</div>
{% endif %}
<!-- Neighborhood Counts List -->
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 border-bottom d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0 fw-bold text-dark">Neighborhoods</h5>
<span class="badge bg-primary-subtle text-primary px-3 py-2 rounded-pill">
{{ neighborhoods|length }} Neighborhoods
</span>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light text-muted">
<tr>
<th class="ps-4 py-3 text-uppercase small ls-1">Neighborhood</th>
<th class="py-3 text-uppercase small ls-1 text-center">Unvisited Households</th>
<th class="pe-4 py-3 text-uppercase small ls-1 text-end">Action</th>
</tr>
</thead>
<tbody>
{% for item in neighborhoods %}
<tr>
<td class="ps-4">
<span class="fw-bold text-dark">{{ item.display_name }}</span>
</td>
<td class="text-center">
<span class="badge bg-light text-dark border px-3 py-2 fw-bold" style="font-size: 1rem;">
{{ item.count }}
</span>
</td>
<td class="pe-4 text-end">
<a href="{% url 'door_visits' %}?city={{ city_filter|urlencode }}&district={{ district_filter|urlencode }}&neighborhood={{ item.neighborhood|default:''|urlencode }}&address={{ address_filter|urlencode }}" class="btn btn-sm btn-primary rounded-3 px-3">
View Households
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center py-5 text-muted">
No data found for the current filters.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -63,6 +63,7 @@ urlpatterns = [
path('door-visits/', views.door_visits, name='door_visits'), path('door-visits/', views.door_visits, name='door_visits'),
path('door-visits/log/', views.log_door_visit, name='log_door_visit'), path('door-visits/log/', views.log_door_visit, name='log_door_visit'),
path('door-visits/history/', views.door_visit_history, name='door_visit_history'), path('door-visits/history/', views.door_visit_history, name='door_visit_history'),
path('door-visits/neighborhoods/', views.neighborhood_counts, name='neighborhood_counts'),
# Call Queue # Call Queue
path('call-queue/', views.call_queue, name='call_queue'), path('call-queue/', views.call_queue, name='call_queue'),

View File

@ -1588,6 +1588,7 @@ def door_visits(request):
tenant = get_object_or_404(Tenant, id=selected_tenant_id) tenant = get_object_or_404(Tenant, id=selected_tenant_id)
city_filter = request.GET.get("city", "").strip()
# Filters from GET parameters # Filters from GET parameters
district_filter = request.GET.get('district', '').strip() district_filter = request.GET.get('district', '').strip()
neighborhood_filter = request.GET.get('neighborhood', '').strip() neighborhood_filter = request.GET.get('neighborhood', '').strip()
@ -1596,6 +1597,8 @@ def door_visits(request):
# Initial queryset: unvisited targeted voters for this tenant # Initial queryset: unvisited targeted voters for this tenant
voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True) voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True)
if city_filter:
voters = voters.filter(city__icontains=city_filter)
# Apply filters if provided # Apply filters if provided
if district_filter: if district_filter:
voters = voters.filter(district=district_filter) voters = voters.filter(district=district_filter)
@ -1671,6 +1674,7 @@ def door_visits(request):
'district_filter': district_filter, 'district_filter': district_filter,
'neighborhood_filter': neighborhood_filter, 'neighborhood_filter': neighborhood_filter,
'address_filter': address_filter, 'address_filter': address_filter,
"city_filter": city_filter,
'map_data_json': json.dumps(map_data), 'map_data_json': json.dumps(map_data),
'GOOGLE_MAPS_API_KEY': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''), 'GOOGLE_MAPS_API_KEY': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''),
'visit_form': DoorVisitLogForm(), 'visit_form': DoorVisitLogForm(),
@ -2076,3 +2080,56 @@ def profile(request):
if request.method == 'POST': if request.method == 'POST':
u_form = UserUpdateForm(request.POST, instance=request.user) u_form = UserUpdateForm(request.POST, instance=request.user)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
def neighborhood_counts(request):
"""
Shows household counts by neighborhood after applying filters from door visits.
"""
selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id:
messages.warning(request, "Please select a campaign first.")
return redirect("index")
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
city_filter = request.GET.get("city", "").strip()
district_filter = request.GET.get('district', '').strip()
neighborhood_filter = request.GET.get('neighborhood', '').strip()
address_filter = request.GET.get('address', '').strip()
voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True)
if city_filter:
voters = voters.filter(city__icontains=city_filter)
if district_filter:
voters = voters.filter(district=district_filter)
if neighborhood_filter:
voters = voters.filter(neighborhood__icontains=neighborhood_filter)
if address_filter:
voters = voters.filter(Q(address__icontains=address_filter) | Q(address_street__icontains=address_filter))
household_qs = voters.values('neighborhood', 'address_street', 'city', 'state', 'zip_code').distinct()
neighborhood_counts_dict = {}
for h in household_qs:
nb = h['neighborhood']
neighborhood_counts_dict[nb] = neighborhood_counts_dict.get(nb, 0) + 1
neighborhood_list = [
{'neighborhood': nb, 'display_name': nb or "Unknown", 'count': count}
for nb, count in neighborhood_counts_dict.items()
]
neighborhood_list.sort(key=lambda x: x['count'], reverse=True)
context = {
'selected_tenant': tenant,
'neighborhoods': neighborhood_list,
'city_filter': city_filter,
'district_filter': district_filter,
'neighborhood_filter': neighborhood_filter,
'address_filter': address_filter,
}
return render(request, 'core/neighborhood_counts.html', context)