"""Dashboard routes - product owner feedback management""" from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, abort, current_app from flask_login import login_required, current_user from app.services.feedback_storage import FeedbackStorageService from app.services.ai_analyzer import ClaudeAnalyzer from app.models.product import Product import os import mimetypes bp = Blueprint('dashboard', __name__) def get_user_product_ids(): """Get list of product IDs accessible to current user Returns: list: Product IDs or None for administrators (access to all) """ if not current_user.is_authenticated: return [] # Administrators have access to all products if current_user.role == 'administrator': return None # None means all products # Product owners see only assigned products return current_user.product_ids def check_product_access(product_id): """Check if current user has access to product Args: product_id: Product ID to check Returns: bool: True if user has access, False otherwise """ if not current_user.is_authenticated: return False # Administrators have access to all products if current_user.role == 'administrator': return True # Product owners see only assigned products return product_id in current_user.product_ids @bp.route('/dashboard') @login_required def list(): """Dashboard - list feedback with filters and search Query parameters: page: Page number (default 1) category: Filter by category status: Filter by status language: Filter by language search: Search query """ try: # Get query parameters page = request.args.get('page', 1, type=int) category = request.args.get('category') status = request.args.get('status') language = request.args.get('language') search_query = request.args.get('search') # Build filters filters = {} if category: filters['category'] = category if status: filters['status'] = status if language: filters['language'] = language # Get product IDs for current user product_ids = get_user_product_ids() current_app.logger.info(f'Dashboard accessed by {current_user.username} (page={page}, filters={filters})') # Load feedback list result = FeedbackStorageService.load_feedback_list( product_ids=product_ids, page=page, per_page=50, filters=filters if filters else None, search_query=search_query ) # Load product names for display all_products = Product.get_all() product_names = {p.product_id: p.name for p in all_products} return render_template( 'dashboard/list.html', feedback_list=result['items'], page=result['page'], pages=result['pages'], total=result['total'], product_names=product_names, filters={ 'category': category, 'status': status, 'language': language, 'search': search_query } ) except Exception as e: current_app.logger.error(f'Error loading dashboard for {current_user.username}: {e}', exc_info=True) abort(500) @bp.route('/feedback/') @login_required def detail(feedback_id): """Feedback detail view Args: feedback_id: Feedback ID to view Returns: Rendered template or 403/404 error """ # First check if feedback exists globally (to distinguish 403 from 404) all_products = Product.get_all() feedback_data = None actual_product_id = None for product in all_products: feedback_data = FeedbackStorageService.load_feedback_detail(product.product_id, feedback_id) if feedback_data: actual_product_id = product.product_id break # If not found globally, return 404 if not feedback_data: abort(404) # Check if user has access to this product if not check_product_access(actual_product_id): abort(403) # Load product info product = Product.get_by_id(actual_product_id) # Check if analysis exists has_analysis = FeedbackStorageService.has_analysis(actual_product_id, feedback_id) # Check if feedback can be analyzed (has text content) can_analyze = bool(feedback_data.get('content')) return render_template( 'dashboard/detail.html', feedback=feedback_data, product=product, has_analysis=has_analysis, can_analyze=can_analyze ) @bp.route('/feedback//status', methods=['POST']) @login_required def update_status(feedback_id): """Update feedback status Args: feedback_id: Feedback ID to update Returns: Redirect to detail page or error """ new_status = request.form.get('status') if not new_status: flash('Status is required', 'error') return redirect(url_for('dashboard.detail', feedback_id=feedback_id)) # Find feedback globally first all_products = Product.get_all() actual_product_id = None for product in all_products: feedback_data = FeedbackStorageService.load_feedback_detail(product.product_id, feedback_id) if feedback_data: actual_product_id = product.product_id break # If not found globally, return 404 if not actual_product_id: abort(404) # Check if user has access to this product if not check_product_access(actual_product_id): abort(403) # Update status success = FeedbackStorageService.update_feedback_status_by_id( actual_product_id, feedback_id, new_status ) if success: flash(f'Status updated to {new_status}', 'success') else: flash('Failed to update status', 'error') return redirect(url_for('dashboard.detail', feedback_id=feedback_id)) @bp.route('/feedback//analyze', methods=['POST']) @login_required def trigger_analysis(feedback_id): """Manually trigger AI analysis for feedback Args: feedback_id: Feedback ID to analyze Returns: Redirect to detail page with flash message """ # Find feedback globally first all_products = Product.get_all() actual_product_id = None feedback_data = None for product in all_products: feedback_data = FeedbackStorageService.load_feedback_detail(product.product_id, feedback_id) if feedback_data: actual_product_id = product.product_id break # If not found globally, return 404 if not actual_product_id or not feedback_data: abort(404) # Check if user has access to this product if not check_product_access(actual_product_id): abort(403) # Check if feedback has text content if not feedback_data.get('content'): flash('Cannot analyze feedback without text content', 'error') return redirect(url_for('dashboard.detail', feedback_id=feedback_id)) # Get product info for owner language product = Product.get_by_id(actual_product_id) try: # Update status to "analyzing" FeedbackStorageService.update_feedback_status_by_id( actual_product_id, feedback_id, 'analyzing' ) # Get API key from app configuration api_key = current_app.config.get('ANTHROPIC_API_KEY') if not api_key: raise Exception("ANTHROPIC_API_KEY not configured in app settings") # Initialize analyzer analyzer = ClaudeAnalyzer(api_key=api_key) # Analyze feedback result = analyzer.analyze_feedback( feedback_text=feedback_data['content'], target_language=product.owner_language, product_id=actual_product_id ) # Save analysis results FeedbackStorageService.save_analysis(actual_product_id, feedback_id, result) # Update status to "analyzed" FeedbackStorageService.update_feedback_status_by_id( actual_product_id, feedback_id, 'analyzed' ) flash('Analysis completed successfully', 'success') except Exception as e: # Update status to "analysis_failed" on error FeedbackStorageService.update_feedback_status_by_id( actual_product_id, feedback_id, 'analysis_failed' ) current_app.logger.error(f"Manual analysis failed for feedback {feedback_id}: {e}") flash(f'Analysis failed: {str(e)}', 'error') return redirect(url_for('dashboard.detail', feedback_id=feedback_id)) @bp.route('/feedback//attachment/') @login_required def download_attachment(feedback_id, filename): """Download attachment file Args: feedback_id: Feedback ID filename: Attachment filename Returns: File download or error """ # Find feedback globally first all_products = Product.get_all() actual_product_id = None for product in all_products: feedback_data = FeedbackStorageService.load_feedback_detail(product.product_id, feedback_id) if feedback_data: actual_product_id = product.product_id break # If not found globally, return 404 if not actual_product_id: abort(404) # Check if user has access to this product if not check_product_access(actual_product_id): abort(403) # Get attachment path attachment_path = FeedbackStorageService.get_attachment_path( actual_product_id, feedback_id, filename ) if not attachment_path: abort(404) # Detect MIME type mime_type, _ = mimetypes.guess_type(filename) if not mime_type: mime_type = 'application/octet-stream' # Send file return send_file( attachment_path, mimetype=mime_type, as_attachment=True, download_name=filename )