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>
110 lines
5.1 KiB
HTML
110 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Feedback Detail{% endblock %}
|
|
|
|
{% block content %}
|
|
<div style="max-width: 900px; margin: 0 auto; padding: 20px;">
|
|
<div style="margin-bottom: 20px;">
|
|
<a href="{{ url_for('dashboard.list') }}" style="color: #007bff; text-decoration: none;">← Back to Dashboard</a>
|
|
</div>
|
|
|
|
<h1>Feedback Detail</h1>
|
|
|
|
<!-- Metadata Section -->
|
|
<div style="background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0;">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px;">
|
|
<div>
|
|
<strong>Feedback ID:</strong>
|
|
<p style="margin: 5px 0; font-family: monospace; font-size: 0.9em;">{{ feedback.feedback_id }}</p>
|
|
</div>
|
|
<div>
|
|
<strong>Product:</strong>
|
|
<p style="margin: 5px 0;">{{ product.name if product else feedback.product_id }}</p>
|
|
</div>
|
|
<div>
|
|
<strong>Status:</strong>
|
|
<p style="margin: 5px 0;">
|
|
<span style="padding: 5px 10px; border-radius: 3px;
|
|
{% if feedback.metadata.status == 'new' %}background: #d1ecf1; color: #0c5460;
|
|
{% elif feedback.metadata.status == 'in_progress' %}background: #fff3cd; color: #856404;
|
|
{% elif feedback.metadata.status == 'resolved' %}background: #d4edda; color: #155724;
|
|
{% else %}background: #e2e3e5; color: #383d41;{% endif %}">
|
|
{{ feedback.metadata.status }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<strong>Category:</strong>
|
|
<p style="margin: 5px 0;">
|
|
<span style="padding: 5px 10px; background: #e9ecef; border-radius: 3px;">
|
|
{{ feedback.metadata.category or 'Uncategorized' }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<strong>Language:</strong>
|
|
<p style="margin: 5px 0;">{{ feedback.metadata.original_language or 'Unknown' }}</p>
|
|
</div>
|
|
<div>
|
|
<strong>Submitted:</strong>
|
|
<p style="margin: 5px 0;">{{ feedback.metadata.submitted_at[:19] if feedback.metadata.submitted_at else 'Unknown' }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Update Status Form -->
|
|
<div style="margin: 20px 0; padding: 15px; background: #fff3cd; border-radius: 5px;">
|
|
<form method="post" action="{{ url_for('dashboard.update_status', feedback_id=feedback.feedback_id) }}" style="display: flex; align-items: center; gap: 10px;">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<label for="status"><strong>Update Status:</strong></label>
|
|
<select name="status" id="status" style="padding: 5px 10px; border: 1px solid #ccc; border-radius: 3px;">
|
|
<option value="new" {% if feedback.metadata.status == 'new' %}selected{% endif %}>New</option>
|
|
<option value="in_progress" {% if feedback.metadata.status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
|
<option value="resolved" {% if feedback.metadata.status == 'resolved' %}selected{% endif %}>Resolved</option>
|
|
<option value="closed" {% if feedback.metadata.status == 'closed' %}selected{% endif %}>Closed</option>
|
|
</select>
|
|
<button type="submit" style="padding: 5px 15px; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">Update</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Original Content -->
|
|
<div style="margin: 30px 0;">
|
|
<h2>Original Feedback</h2>
|
|
<div style="background: white; border: 1px solid #dee2e6; border-radius: 5px; padding: 20px; white-space: pre-wrap;">{{ feedback.content }}</div>
|
|
</div>
|
|
|
|
<!-- Analysis (if exists) -->
|
|
{% if feedback.analysis %}
|
|
<div style="margin: 30px 0;">
|
|
<h2>AI Analysis</h2>
|
|
<div style="background: white; border: 1px solid #dee2e6; border-radius: 5px; padding: 20px;">
|
|
{{ feedback.analysis|safe }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Attachments -->
|
|
{% if feedback.attachments %}
|
|
<div style="margin: 30px 0;">
|
|
<h2>Attachments ({{ feedback.attachments|length }})</h2>
|
|
<ul style="list-style: none; padding: 0;">
|
|
{% for attachment in feedback.attachments %}
|
|
<li style="margin: 10px 0; padding: 10px; background: #f8f9fa; border-radius: 3px;">
|
|
<a href="{{ url_for('dashboard.download_attachment', feedback_id=feedback.feedback_id, filename=attachment) }}"
|
|
style="color: #007bff; text-decoration: none; display: flex; align-items: center; gap: 10px;">
|
|
<span style="font-size: 1.2em;">📎</span>
|
|
<span>{{ attachment }}</span>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% else %}
|
|
<div style="margin: 30px 0;">
|
|
<h2>Attachments</h2>
|
|
<p style="color: #666;">No attachments</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|