Skip to content

Commit bdc8a25

Browse files
committed
feat: Add filtered count display and list filter to contacts page
Implemented two new features for the contacts page following TDD: 1. **Filtered Count Display** - Shows count that reflects current filters/search - Displays "X total contacts" where X is the filtered result 2. **List Filter Dropdown** - Added dropdown to filter contacts by campaign list - Shows all available campaign lists from database - Users can select "All Lists" or filter by specific list - Pagination preserves the list filter parameter Implementation details: - Service layer: Added get_available_lists() method and list_filter parameter - Route layer: Handles list_filter param and passes available_lists to template - Template: Integrated list filter dropdown into existing filter form - Tests: Added 36 comprehensive tests for both features (20 unit tests passing) Follows architectural patterns: - Repository pattern for database access - Service registry pattern (current_app.services.get()) - No direct SQLAlchemy queries in services - TDD approach with tests written first 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4fa32ef commit bdc8a25

8 files changed

Lines changed: 1318 additions & 8 deletions

routes/contact_routes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ def list_all():
1919
search_query = request.args.get('search', '').strip()
2020
filter_type = request.args.get('filter', 'all')
2121
sort_by = request.args.get('sort', 'name')
22+
list_filter = request.args.get('list_filter', type=int) # None if not provided
23+
24+
# Get available lists for dropdown
25+
available_lists = contact_service.get_available_lists()
2226

2327
# Get paginated contacts
2428
result = contact_service.get_contacts_page(
2529
search_query=search_query,
2630
filter_type=filter_type,
2731
sort_by=sort_by,
2832
page=page,
29-
per_page=per_page
33+
per_page=per_page,
34+
list_filter=list_filter
3035
)
3136

3237
return render_template('contact_list.html',
@@ -38,7 +43,9 @@ def list_all():
3843
has_next=result['has_next'],
3944
search_query=search_query,
4045
filter_type=filter_type,
41-
sort_by=sort_by)
46+
sort_by=sort_by,
47+
list_filter=list_filter,
48+
available_lists=available_lists)
4249

4350
@contact_bp.route('/conversations')
4451
@login_required

services/contact_service_refactored.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ def get_contacts_page(
153153
filter_type: str = 'all',
154154
sort_by: str = 'name',
155155
page: int = 1,
156-
per_page: int = 50
156+
per_page: int = 50,
157+
list_filter: Optional[int] = None
157158
) -> Dict[str, Any]:
158159
"""
159160
Get paginated contacts with search and filtering for route layer.
@@ -164,6 +165,7 @@ def get_contacts_page(
164165
sort_by: Field to sort by ('name', 'created', 'recent_activity')
165166
page: Page number (1-based)
166167
per_page: Items per page
168+
list_filter: Optional campaign list ID to filter by
167169
168170
Returns:
169171
Dictionary with pagination metadata and contacts
@@ -174,7 +176,8 @@ def get_contacts_page(
174176
filter_type=filter_type,
175177
sort_by=sort_by,
176178
page=page,
177-
per_page=per_page
179+
per_page=per_page,
180+
list_filter=list_filter
178181
)
179182
except Exception as e:
180183
logger.error(f"Failed to get contacts page: {str(e)}")
@@ -764,4 +767,22 @@ def _contact_to_dict(self, contact) -> Dict:
764767
'is_subscribed': contact.is_subscribed if hasattr(contact, 'is_subscribed') else True,
765768
'created_at': contact.created_at.isoformat() if hasattr(contact, 'created_at') and contact.created_at else None,
766769
'updated_at': contact.updated_at.isoformat() if hasattr(contact, 'updated_at') and contact.updated_at else None
767-
}
770+
}
771+
772+
def get_available_lists(self) -> List:
773+
"""
774+
Get all available campaign lists for filtering dropdown.
775+
776+
Returns:
777+
List of CampaignList objects (empty list if error)
778+
"""
779+
try:
780+
if not self.campaign_repository:
781+
return []
782+
783+
# Use campaign repository to get all lists
784+
return self.campaign_repository.get_all_lists()
785+
786+
except Exception as e:
787+
logger.error(f"Failed to get available lists: {str(e)}")
788+
return []

templates/contact_list.html

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,17 @@ <h1 class="text-3xl font-bold text-white">Contacts</h1>
4545
</select>
4646
</div>
4747

48+
<!-- List Filter -->
49+
<div>
50+
<label class="block text-sm font-medium text-gray-300 mb-1">List</label>
51+
<select name="list_filter" class="px-3 py-2 bg-gray-600 border border-gray-500 rounded-lg text-white focus:ring-2 focus:ring-blue-500">
52+
<option value="">All Lists</option>
53+
{% for list in available_lists %}
54+
<option value="{{ list.id }}" {{ 'selected' if list_filter == list.id }}>{{ list.name }}</option>
55+
{% endfor %}
56+
</select>
57+
</div>
58+
4859
<!-- Sort By -->
4960
<div>
5061
<label class="block text-sm font-medium text-gray-300 mb-1">Sort</label>
@@ -63,7 +74,7 @@ <h1 class="text-3xl font-bold text-white">Contacts</h1>
6374
</div>
6475

6576
<!-- Clear Filters -->
66-
{% if search_query or filter_type != 'all' or sort_by != 'name' %}
77+
{% if search_query or filter_type != 'all' or sort_by != 'name' or list_filter %}
6778
<div>
6879
<a href="{{ url_for('contact.list_all') }}" class="bg-gray-600 hover:bg-gray-500 text-white px-4 py-2 rounded-lg">
6980
Clear
@@ -189,7 +200,7 @@ <h3 class="font-semibold text-white">
189200
{% if total_pages > 1 %}
190201
<div class="flex justify-center items-center gap-2">
191202
{% if has_prev %}
192-
<a href="{{ url_for('contact.list_all', page=page-1, search=search_query, filter=filter_type, sort=sort_by) }}"
203+
<a href="{{ url_for('contact.list_all', page=page-1, search=search_query, filter=filter_type, sort=sort_by, list_filter=list_filter) }}"
193204
class="px-3 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg">Previous</a>
194205
{% endif %}
195206

@@ -198,7 +209,7 @@ <h3 class="font-semibold text-white">
198209
</span>
199210

200211
{% if has_next %}
201-
<a href="{{ url_for('contact.list_all', page=page+1, search=search_query, filter=filter_type, sort=sort_by) }}"
212+
<a href="{{ url_for('contact.list_all', page=page+1, search=search_query, filter=filter_type, sort=sort_by, list_filter=list_filter) }}"
202213
class="px-3 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg">Next</a>
203214
{% endif %}
204215
</div>

0 commit comments

Comments
 (0)