"""Output Manager Dialog for ChefSystem. Dialog for managing outputs associated with a recipe. """ import logging from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QListWidgetItem, QTextEdit, QLabel, QFileDialog, QMessageBox, QInputDialog, QLineEdit ) from PyQt6.QtCore import Qt from src.database.models import Recipe from src.services.output_service import OutputService from src.utils.file_utils import format_file_size logger = logging.getLogger(__name__) class OutputManager(QDialog): """Dialog for managing recipe outputs. Displays list of outputs with Add/Open/Delete actions and execution notes editor. """ def __init__(self, db_manager, recipe: Recipe, parent=None): """Initialize the output manager dialog. Args: db_manager: DatabaseManager instance. recipe: Recipe instance to manage outputs for. parent: Parent widget. """ super().__init__(parent) self.db_manager = db_manager self.recipe = recipe self.output_service = OutputService(db_manager) self.current_output = None self.setWindowTitle(f"Manage Outputs: {recipe.name}") self.setMinimumSize(800, 600) # UI components self.outputs_list: QListWidget = None self.notes_edit: QTextEdit = None self.add_button: QPushButton = None self.open_button: QPushButton = None self.delete_button: QPushButton = None self._setup_ui() self._load_outputs() def _setup_ui(self) -> None: """Set up the UI components.""" main_layout = QVBoxLayout() # Header header = QLabel(f"

Outputs for: {self.recipe.name}

") main_layout.addWidget(header) # Search box search_label = QLabel("Search Outputs:") main_layout.addWidget(search_label) self.search_box = QLineEdit() self.search_box.setPlaceholderText("Search by filename or notes...") self.search_box.textChanged.connect(self.on_search_changed) main_layout.addWidget(self.search_box) # Outputs list list_label = QLabel("Output Files:") main_layout.addWidget(list_label) self.outputs_list = QListWidget() self.outputs_list.itemSelectionChanged.connect(self.on_output_selected) self.outputs_list.itemDoubleClicked.connect(self.on_output_double_clicked) main_layout.addWidget(self.outputs_list) # Action buttons button_layout = QHBoxLayout() self.add_button = QPushButton("Add Output") self.add_button.clicked.connect(self.on_add_output) button_layout.addWidget(self.add_button) self.open_button = QPushButton("Open File") self.open_button.setEnabled(False) self.open_button.clicked.connect(self.on_open_output) button_layout.addWidget(self.open_button) self.delete_button = QPushButton("Delete") self.delete_button.setEnabled(False) self.delete_button.clicked.connect(self.on_delete_output) button_layout.addWidget(self.delete_button) button_layout.addStretch() main_layout.addLayout(button_layout) # Execution notes section notes_label = QLabel("Execution Notes:") main_layout.addWidget(notes_label) self.notes_edit = QTextEdit() self.notes_edit.setMaximumHeight(100) self.notes_edit.setReadOnly(True) self.notes_edit.setPlaceholderText("Select an output to view execution notes") main_layout.addWidget(self.notes_edit) # Close button close_button = QPushButton("Close") close_button.clicked.connect(self.accept) main_layout.addWidget(close_button) self.setLayout(main_layout) def _load_outputs(self, search_query: str = "") -> None: """Load outputs for the recipe from database. Args: search_query: Optional search query to filter outputs. """ try: # Get all outputs for this recipe outputs = self.output_service.get_recipe_outputs(self.recipe.id) # Filter by search query if provided if search_query and search_query.strip(): query_lower = search_query.strip().lower() outputs = [ output for output in outputs if query_lower in output.filename.lower() or (output.execution_notes and query_lower in output.execution_notes.lower()) ] self.outputs_list.clear() if not outputs: item = QListWidgetItem("No outputs yet") item.setFlags(Qt.ItemFlag.NoItemFlags) self.outputs_list.addItem(item) logger.info(f"No outputs found for recipe {self.recipe.id}") return for output in outputs: # Format: filename (type, size) - date date_str = output.generated_at.strftime("%Y-%m-%d %H:%M") if output.generated_at else "-" size_str = format_file_size(output.file_size) if output.file_size else "0 B" item_text = f"{output.filename} ({output.file_type}, {size_str}) - {date_str}" item = QListWidgetItem(item_text) item.setData(Qt.ItemDataRole.UserRole, output.id) self.outputs_list.addItem(item) logger.info(f"Loaded {len(outputs)} outputs for recipe {self.recipe.id}") except Exception as e: logger.error(f"Error loading outputs: {e}", exc_info=True) QMessageBox.critical(self, "Error", f"Failed to load outputs: {e}") def on_output_selected(self) -> None: """Handle output selection change.""" selected_items = self.outputs_list.selectedItems() if not selected_items: self.current_output = None self.notes_edit.clear() self.open_button.setEnabled(False) self.delete_button.setEnabled(False) return # Get selected output ID item = selected_items[0] output_id = item.data(Qt.ItemDataRole.UserRole) if output_id is None: # "No outputs yet" placeholder return # Load output try: output = self.output_service.get_output_by_id(output_id) if output: self.current_output = output self.notes_edit.setPlainText(output.execution_notes if output.execution_notes else "No notes") self.open_button.setEnabled(True) self.delete_button.setEnabled(True) logger.debug(f"Selected output: {output.filename}") except Exception as e: logger.error(f"Error loading output {output_id}: {e}", exc_info=True) def on_output_double_clicked(self, item: QListWidgetItem) -> None: """Handle double-click on output item (opens file). Args: item: The clicked list item. """ output_id = item.data(Qt.ItemDataRole.UserRole) if output_id: self.on_open_output() def on_add_output(self) -> None: """Handle add output action.""" try: # Open file picker dialog file_path, _ = QFileDialog.getOpenFileName( self, "Select Output File", "", "All Files (*.*)" ) if not file_path: # User cancelled logger.info("Add output cancelled") return # Ask for execution notes notes, ok = QInputDialog.getMultiLineText( self, "Execution Notes", "Enter optional notes about this output:", "" ) if not ok: # User cancelled notes dialog logger.info("Add output cancelled at notes input") return # Save output using service output = self.output_service.save_output( self.recipe.id, file_path, notes ) if output: logger.info(f"Added output: {output.filename}") QMessageBox.information( self, "Success", f"Output '{output.filename}' added successfully!" ) # Reload outputs list self._load_outputs() else: QMessageBox.critical( self, "Error", "Failed to add output. Check the logs for details." ) except Exception as e: logger.error(f"Error adding output: {e}", exc_info=True) QMessageBox.critical(self, "Error", f"Failed to add output: {e}") def on_open_output(self) -> None: """Handle open output action.""" if not self.current_output: return try: success = self.output_service.open_output(self.current_output.id) if not success: QMessageBox.warning( self, "Error", f"Failed to open '{self.current_output.filename}'.\n\n" "The file may not exist or no application is configured to open this file type." ) except Exception as e: logger.error(f"Error opening output: {e}", exc_info=True) QMessageBox.critical(self, "Error", f"Failed to open output: {e}") def on_delete_output(self) -> None: """Handle delete output action.""" if not self.current_output: return # Show confirmation dialog reply = QMessageBox.question( self, "Confirm Delete", f"Are you sure you want to delete output '{self.current_output.filename}'?\n\n" "This will delete both the database record and the file from disk.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: success = self.output_service.delete_output(self.current_output.id) if success: logger.info(f"Deleted output: {self.current_output.filename}") QMessageBox.information( self, "Success", "Output deleted successfully" ) # Clear selection and reload self.current_output = None self._load_outputs() else: QMessageBox.warning( self, "Error", "Failed to delete output. Check the logs for details." ) except Exception as e: logger.error(f"Error deleting output: {e}", exc_info=True) QMessageBox.critical(self, "Error", f"Failed to delete output: {e}") def on_search_changed(self, text: str) -> None: """Handle search box text change. Args: text: Search query text. """ self._load_outputs(search_query=text)