"""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)