Files
gurixandClaude adbfd23c26 Implement Phase 5: Product Owner Dashboard (User Story 3)
Add complete dashboard functionality for product owners and administrators to view, filter, search, and manage feedback submissions following Test-First Discipline.

Tests (T093-T105):
- Add 12 contract tests for dashboard routes (authentication, listing, filtering, search, detail view, status updates, attachment downloads, access control)
- Add 2 integration tests for complete dashboard workflow and access control enforcement
- All tests written first and verified to fail before implementation

Services (T111-T118):
- Enhance FeedbackStorageService with load_feedback_list() for pagination, filtering, searching, and sorting
- Add load_feedback_detail() to load complete feedback with attachments and analysis
- Add update_feedback_status_by_id() for status management
- Add get_attachment_path() with path traversal prevention

Routes (T119-T134):
- Implement GET /dashboard with filters, search, and pagination (50 items/page)
- Implement GET /feedback/<id> detail view with role-based access control
- Implement POST /feedback/<id>/status for status updates
- Implement GET /feedback/<id>/attachment/<filename> for secure file downloads
- Add access control helpers (administrators see all products, owners see only assigned)

Templates (T135-T136):
- Create dashboard/list.html with filter form, search, and pagination
- Create dashboard/detail.html with status update form and attachment links
- Create error_403.html for access denied
- Create error_404.html for not found

Integration & Bug Fixes:
- Update auth routes to remove /auth prefix and redirect to dashboard after login
- Update Feedback.VALID_STATUSES to include dashboard statuses (in_progress, resolved, closed)
- Register error handlers for 403 and 404 in app factory
- Fix test fixtures to use correct users.yaml format and User.hash_password()

Test Results: 39 passed, 1 skipped (all Phase 5 tests passing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 20:11:16 +02:00

132 lines
6.7 KiB
HTML

{% extends "base.html" %}
{% block title %}Feedback Dashboard{% endblock %}
{% block content %}
<div style="max-width: 1200px; margin: 0 auto; padding: 20px;">
<h1>Feedback Dashboard</h1>
<!-- Filters and Search -->
<form method="get" action="{{ url_for('dashboard.list') }}" style="margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 5px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;">
<div>
<label for="category">Category:</label>
<select name="category" id="category">
<option value="">All Categories</option>
<option value="bug" {% if filters.category == 'bug' %}selected{% endif %}>Bug</option>
<option value="feature_request" {% if filters.category == 'feature_request' %}selected{% endif %}>Feature Request</option>
<option value="question" {% if filters.category == 'question' %}selected{% endif %}>Question</option>
<option value="complaint" {% if filters.category == 'complaint' %}selected{% endif %}>Complaint</option>
<option value="praise" {% if filters.category == 'praise' %}selected{% endif %}>Praise</option>
<option value="other" {% if filters.category == 'other' %}selected{% endif %}>Other</option>
</select>
</div>
<div>
<label for="status">Status:</label>
<select name="status" id="status">
<option value="">All Statuses</option>
<option value="new" {% if filters.status == 'new' %}selected{% endif %}>New</option>
<option value="in_progress" {% if filters.status == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="resolved" {% if filters.status == 'resolved' %}selected{% endif %}>Resolved</option>
<option value="closed" {% if filters.status == 'closed' %}selected{% endif %}>Closed</option>
</select>
</div>
<div>
<label for="search">Search:</label>
<input type="text" name="search" id="search" value="{{ filters.search or '' }}" placeholder="Search feedback...">
</div>
<div style="display: flex; align-items: flex-end; gap: 5px;">
<button type="submit" style="padding: 8px 15px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;">Filter</button>
<a href="{{ url_for('dashboard.list') }}" style="padding: 8px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 3px; display: inline-block;">Clear</a>
</div>
</div>
</form>
<!-- Results Summary -->
<p style="margin: 10px 0; color: #666;">
Showing {{ feedback_list|length }} of {{ total }} feedback items
{% if filters.category or filters.status or filters.search %}
(filtered)
{% endif %}
</p>
<!-- Feedback List -->
{% if feedback_list %}
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<thead>
<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
<th style="padding: 12px; text-align: left;">ID</th>
<th style="padding: 12px; text-align: left;">Product</th>
<th style="padding: 12px; text-align: left;">Preview</th>
<th style="padding: 12px; text-align: left;">Category</th>
<th style="padding: 12px; text-align: left;">Status</th>
<th style="padding: 12px; text-align: left;">Date</th>
<th style="padding: 12px; text-align: left;">Actions</th>
</tr>
</thead>
<tbody>
{% for feedback in feedback_list %}
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 12px; font-family: monospace; font-size: 0.9em;">
{{ feedback.feedback_id[:8] }}...
</td>
<td style="padding: 12px;">
{{ product_names.get(feedback.product_id, feedback.product_id) }}
</td>
<td style="padding: 12px; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ feedback.content_preview }}
</td>
<td style="padding: 12px;">
<span style="padding: 3px 8px; background: #e9ecef; border-radius: 3px; font-size: 0.9em;">
{{ feedback.category }}
</span>
</td>
<td style="padding: 12px;">
<span style="padding: 3px 8px; border-radius: 3px; font-size: 0.9em;
{% if feedback.status == 'new' %}background: #d1ecf1; color: #0c5460;
{% elif feedback.status == 'in_progress' %}background: #fff3cd; color: #856404;
{% elif feedback.status == 'resolved' %}background: #d4edda; color: #155724;
{% else %}background: #e2e3e5; color: #383d41;{% endif %}">
{{ feedback.status }}
</span>
</td>
<td style="padding: 12px; font-size: 0.9em; color: #666;">
{{ feedback.submitted_at[:10] if feedback.submitted_at else 'Unknown' }}
</td>
<td style="padding: 12px;">
<a href="{{ url_for('dashboard.detail', feedback_id=feedback.feedback_id) }}"
style="color: #007bff; text-decoration: none;">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Pagination -->
{% if pages > 1 %}
<div style="margin: 20px 0; text-align: center;">
{% if page > 1 %}
<a href="{{ url_for('dashboard.list', page=page-1, category=filters.category, status=filters.status, search=filters.search) }}"
style="padding: 8px 12px; margin: 0 2px; background: #007bff; color: white; text-decoration: none; border-radius: 3px;">Previous</a>
{% endif %}
<span style="padding: 8px 12px; margin: 0 5px;">Page {{ page }} of {{ pages }}</span>
{% if page < pages %}
<a href="{{ url_for('dashboard.list', page=page+1, category=filters.category, status=filters.status, search=filters.search) }}"
style="padding: 8px 12px; margin: 0 2px; background: #007bff; color: white; text-decoration: none; border-radius: 3px;">Next</a>
{% endif %}
</div>
{% endif %}
{% else %}
<p style="margin: 40px 0; text-align: center; color: #666;">
No feedback found.
</p>
{% endif %}
</div>
{% endblock %}