""" Recent Results Panel Displays a table of recent analysis results from the database. Supports sorting, scrolling, and all data display. """ from datetime import datetime from typing import List, Dict, Optional, Callable import time from PyQt5.QtWidgets import (QGroupBox, QVBoxLayout, QTableView, QHeaderView, QPushButton) from PyQt5.QtCore import Qt, QAbstractTableModel, QVariant, QSortFilterProxyModel, pyqtSignal from PyQt5.QtGui import QFont, QColor, QBrush from resources.styles import (GROUP_BOX_STYLE, TABLE_STYLE, get_ripeness_color, get_grade_color) from utils.config import TABLE_ROW_HEIGHT def get_local_datetime_string() -> str: """ Get current datetime string formatted in the computer's local timezone. Uses the system's local timezone for display. Returns: str: Formatted datetime string in format "YYYY-MM-DD HH:MM:SS" """ return datetime.now().strftime("%Y-%m-%d %H:%M:%S") class AnalysisTableModel(QAbstractTableModel): """ Custom table model for analysis results with sorting support. Supports automatic type detection for sorting: - Text: alphabetical sort - Numbers: numeric sort - Dates: chronological sort """ def __init__(self, data: List[List[str]] = None): super().__init__() self.data = data if data else [] self.headers = ["Report ID", "Grade", "Time (s)", "Created", "View"] def rowCount(self, parent=None) -> int: """Return number of rows.""" return len(self.data) def columnCount(self, parent=None) -> int: """Return number of columns.""" return len(self.headers) def data(self, index, role=Qt.DisplayRole): """Get data for a specific cell.""" if not index.isValid(): return QVariant() row = index.row() col = index.column() if row < 0 or row >= len(self.data): return QVariant() # View column (index 4) has no data in the model - it's rendered via setIndexWidget if col >= len(self.data[row]): return QVariant() cell_data = self.data[row][col] if role == Qt.DisplayRole: return str(cell_data) # Color coding for Grade column elif role == Qt.ForegroundRole: if col == 1: # Grade column color = get_grade_color(str(cell_data)) return QBrush(QColor(color)) # Font styling for Grade column elif role == Qt.FontRole: if col == 1: # Grade column font = QFont("Arial", 12, QFont.Bold) return font return QFont("Arial", 12) return QVariant() def headerData(self, section, orientation, role=Qt.DisplayRole): """Get header data.""" if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.headers[section] return QVariant() def set_data(self, data: List[List[str]]): """Update table data.""" self.beginResetModel() self.data = data self.endResetModel() def sort(self, column: int, order: Qt.SortOrder = Qt.AscendingOrder): """Sort data by column with automatic type detection.""" if not self.data: return # Detect column type col_type = self._detect_column_type(column) # Skip sorting for non-sortable columns if col_type == "none": return # Sort based on type reverse = (order == Qt.DescendingOrder) if col_type == "number": self.data.sort(key=lambda row: self._to_float(row[column]), reverse=reverse) elif col_type == "date": self.data.sort(key=lambda row: self._to_datetime(row[column]), reverse=reverse) else: # text self.data.sort(key=lambda row: str(row[column]).upper(), reverse=reverse) self.layoutChanged.emit() def _detect_column_type(self, column: int) -> str: """Detect column data type.""" if not self.data: return "text" # View column (index 4) should not be sorted if column == 4: return "none" # Check column 2 (Time) if column == 2: return "number" # Check columns 3 (Timestamps) if column == 3: return "date" # Check column 1 (Grade) - single letter or N/A if column == 1: return "text" return "text" def _to_float(self, value) -> float: """Convert value to float for numeric sorting.""" try: return float(str(value).replace("N/A", "0")) except: return 0.0 def _to_datetime(self, value) -> datetime: """Convert value to datetime for date sorting.""" try: # Handle various datetime formats value_str = str(value).strip() if not value_str or value_str == "N/A": return datetime.min # Try parsing as datetime for fmt in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"]: try: return datetime.strptime(value_str[:19], fmt.replace(".%f", "")) except: pass return datetime.min except: return datetime.min class RecentResultsPanel(QGroupBox): """ Panel displaying recent analysis results in a table. Features: - Shows all results (not limited to 5) - Sortable columns (click headers) - Automatic type detection (text, number, date) - Scrollable for large datasets - Latest results at top by default (sorted by Created descending) - Color-coded grades (A=green, B=blue, C=red) - View buttons to load analyses in Reports tab Data is loaded from the SQLite database automatically. """ # Signal emitted when user clicks View button for an analysis view_analysis_requested = pyqtSignal(str) # Emits report_id def __init__(self, data_manager: Optional[object] = None): """ Initialize the recent results panel. Args: data_manager: DataManager instance for loading from database (optional) """ super().__init__("Recent Analysis Results") self.setStyleSheet(GROUP_BOX_STYLE) self.results_data = [] # Store all results self.data_manager = data_manager self.model = None self.proxy_model = None self.table = None self.init_ui() self._load_initial_data() def init_ui(self): """Initialize the UI components.""" layout = QVBoxLayout() # Create custom table model self.model = AnalysisTableModel(self.results_data) # Create proxy model for sorting self.proxy_model = QSortFilterProxyModel() self.proxy_model.setSourceModel(self.model) self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive) # Create table view self.table = QTableView() self.table.setModel(self.proxy_model) self.table.setFont(QFont("Arial", 12)) self.table.setStyleSheet(TABLE_STYLE) # Table settings self.table.setAlternatingRowColors(True) self.table.horizontalHeader().setStretchLastSection(False) self.table.verticalHeader().setVisible(False) self.table.setSelectionBehavior(QTableView.SelectRows) self.table.verticalHeader().setDefaultSectionSize(TABLE_ROW_HEIGHT) # Enable sorting self.table.setSortingEnabled(True) self.table.horizontalHeader().setSectionsClickable(True) # Resize columns to content self.table.resizeColumnsToContents() # Configure column resize modes # Make first column (Report ID) wider and stretchable self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) # Set Created column (index 3) to fixed width to show full datetime self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) self.table.setColumnWidth(3, 190) # Set View column to fixed width self.table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents) self.table.setColumnWidth(4, 80) # Connect signal for button refresh after sorting self.proxy_model.layoutChanged.connect(self._add_view_buttons) layout.addWidget(self.table) self.setLayout(layout) def _load_initial_data(self): """Load initial data from database or use sample data as fallback.""" if self.data_manager: self.refresh_from_database() else: # Fallback to sample data if data_manager not available self._load_sample_data() def _load_sample_data(self): """Load sample data for demonstration.""" sample_data = [ ["DUR-20260121-135138-2", "C", "3.50", "2026-01-21 13:51:38"], ["DUR-20260121-135138-1", "B", "2.75", "2026-01-21 13:51:35"], ["DUR-20260121-135138-0", "A", "1.95", "2026-01-21 13:51:32"], ["DUR-20260121-135053", "B", "4.23", "2026-01-21 13:50:53"], ["DUR-20260121-135052", "A", "5.50", "2026-01-21 13:50:52"] ] self.results_data = sample_data self.model.set_data(self.results_data) self._sort_by_created_descending() self._add_view_buttons() def add_result(self, report_id: str, grade: str, processing_time: float, created_at: str = None): """ Add a new result to the table. Args: report_id: Report ID from analysis (e.g., "DUR-20260121-135153") grade: Grade (A, B, or C) processing_time: Total processing time in seconds created_at: Creation time (optional, defaults to now in local timezone) """ # Use current local time if not provided if not created_at: created_at = get_local_datetime_string() # Format time string time_str = f"{processing_time:.2f}" row_data = [report_id, grade, time_str, created_at] self.results_data.append(row_data) # Update table display self.model.set_data(self.results_data) self._sort_by_created_descending() self._add_view_buttons() def refresh_from_database(self): """Refresh the table by loading recent analyses from the database.""" if not self.data_manager: print("Data manager not initialized, cannot refresh from database") return try: # Get recent analyses from database recent_analyses = self.data_manager.list_recent_analyses(limit=50) # Clear existing data self.results_data.clear() # Add each analysis to results for analysis in recent_analyses: # Handle None processing_time processing_time = analysis.get('processing_time') if processing_time is None: time_str = 'N/A' else: time_str = f"{processing_time:.2f}" grade = analysis.get('overall_grade') if grade is None: grade = 'N/A' row_data = [ analysis.get('report_id', 'N/A'), grade, time_str, analysis.get('created_at', '') ] self.results_data.append(row_data) print(f"Added row: {row_data}") # Update table display self.model.set_data(self.results_data) self._sort_by_created_descending() self._add_view_buttons() print(f"✓ Loaded {len(self.results_data)} analyses from database") except Exception as e: print(f"Error refreshing from database: {e}") import traceback traceback.print_exc() # Fallback to sample data self._load_sample_data() def _sort_by_created_descending(self): """Sort table by Created column (index 3) in descending order (latest first).""" # Column 3 is "Created" self.proxy_model.sort(3, Qt.DescendingOrder) def _add_view_buttons(self): """ Add View buttons to the View column (index 4) for each row. Buttons are placed using setIndexWidget for each row. """ if not self.table: return try: # Get the number of rows in the proxy model (after sorting/filtering) row_count = self.proxy_model.rowCount() # Clear existing buttons by removing widgets for row in range(row_count): self.table.setIndexWidget(self.proxy_model.index(row, 4), None) # Add new buttons for row in range(row_count): # Get the report_id from column 0 of the proxy model report_id_index = self.proxy_model.index(row, 0) report_id = self.proxy_model.data(report_id_index, Qt.DisplayRole) # Create the View button view_btn = QPushButton("View") view_btn.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; border: none; border-radius: 3px; padding: 5px 10px; font-size: 11px; font-weight: bold; } QPushButton:hover { background-color: #2980b9; } QPushButton:pressed { background-color: #1f618d; } """) # Use lambda with default argument to capture report_id correctly view_btn.clicked.connect(lambda checked=False, rid=report_id: self._on_view_clicked(rid)) # Set the button in the View column self.table.setIndexWidget(self.proxy_model.index(row, 4), view_btn) except Exception as e: print(f"Error adding view buttons: {e}") import traceback traceback.print_exc() def _on_view_clicked(self, report_id: str): """ Handle view button click. Args: report_id: The report ID to view """ # Validate report_id if not report_id or report_id == 'N/A': print(f"Invalid report_id: {report_id}") return # Emit signal to notify main window self.view_analysis_requested.emit(report_id) def clear_results(self): """Clear all results from the table and storage.""" self.results_data.clear() self.model.set_data([]) def get_all_results(self) -> List[List[str]]: """ Get all stored results. Returns: List of result rows """ return self.results_data.copy()