""" Schedules Widget for Python Scheduler Central scheduling hub - schedule individual jobs or groups """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QHeaderView, QLabel, QMessageBox, QMenu ) from PyQt6.QtCore import Qt import socket from PyQt6.QtGui import QAction from loguru import logger from core.database import SchedulerDatabase from core.config import SchedulerConfig from gui.schedule_editor_dialog import ScheduleEditorDialog class SchedulesWidget(QWidget): """Schedules list and management widget""" def __init__(self, db: SchedulerDatabase, config: SchedulerConfig): super().__init__() self.db = db self.config = config self._setup_ui() self.refresh() def _setup_ui(self): """Setup UI""" layout = QVBoxLayout(self) # Header header_layout = QHBoxLayout() header_label = QLabel("Schedules - When to Execute Jobs") header_label.setStyleSheet("font-size: 14px; font-weight: bold;") header_layout.addWidget(header_label) header_layout.addStretch() # Refresh button self.refresh_btn = QPushButton("Refresh") self.refresh_btn.clicked.connect(self.refresh) header_layout.addWidget(self.refresh_btn) layout.addLayout(header_layout) # Action buttons actions_layout = QHBoxLayout() self.new_schedule_btn = QPushButton("New Schedule") self.new_schedule_btn.setStyleSheet("background-color: #4CAF50; color: white; padding: 5px 15px;") self.new_schedule_btn.clicked.connect(self._new_schedule) actions_layout.addWidget(self.new_schedule_btn) self.edit_schedule_btn = QPushButton("Edit") self.edit_schedule_btn.setEnabled(False) self.edit_schedule_btn.clicked.connect(self._edit_schedule) actions_layout.addWidget(self.edit_schedule_btn) self.delete_schedule_btn = QPushButton("Delete") self.delete_schedule_btn.setStyleSheet("background-color: #f44336; color: white; padding: 5px 15px;") self.delete_schedule_btn.setEnabled(False) self.delete_schedule_btn.clicked.connect(self._delete_schedule) actions_layout.addWidget(self.delete_schedule_btn) actions_layout.addStretch() layout.addLayout(actions_layout) # Schedules table self.table = QTableWidget() self.table.setColumnCount(6) self.table.setHorizontalHeaderLabels([ "ID", "Name", "Type", "Target", "Schedule", "Enabled" ]) self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setAlternatingRowColors(True) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # Selection change self.table.itemSelectionChanged.connect(self._on_selection_changed) layout.addWidget(self.table) def refresh(self): """Refresh schedules list""" try: schedules = self.db.get_all_schedules(hostname=socket.gethostname()) self.table.setRowCount(len(schedules)) for row, schedule in enumerate(schedules): # ID self.table.setItem(row, 0, QTableWidgetItem(str(schedule['id']))) # Name self.table.setItem(row, 1, QTableWidgetItem(schedule['name'])) # Type type_map = { 'job': 'Job', 'group': 'Group', 'backup_profile': 'Backup', 'sync_profile': 'Sync' } type_text = type_map.get(schedule['target_type'], schedule['target_type']) self.table.setItem(row, 2, QTableWidgetItem(type_text)) # Target target_name = schedule.get('target_name', f"ID {schedule['target_id']}") self.table.setItem(row, 3, QTableWidgetItem(target_name)) # Schedule schedule_text = self._format_schedule(schedule['schedule_type'], schedule['schedule_config']) self.table.setItem(row, 4, QTableWidgetItem(schedule_text)) # Enabled enabled_text = "Yes" if schedule['enabled'] else "No" enabled_item = QTableWidgetItem(enabled_text) if schedule['enabled']: enabled_item.setForeground(Qt.GlobalColor.darkGreen) else: enabled_item.setForeground(Qt.GlobalColor.red) self.table.setItem(row, 5, enabled_item) logger.debug(f"Refreshed schedules table: {len(schedules)} schedules") except Exception as e: logger.error(f"Error refreshing schedules: {e}") def _format_schedule(self, schedule_type: str, schedule_config: str) -> str: """Format schedule config for display""" try: import json config = json.loads(schedule_config) if isinstance(schedule_config, str) else schedule_config if schedule_type == 'cron': return f"Cron: {config.get('cron', 'N/A')}" elif schedule_type == 'interval': parts = [] for key in ['weeks', 'days', 'hours', 'minutes', 'seconds']: if key in config: parts.append(f"{config[key]} {key}") return f"Every {', '.join(parts)}" elif schedule_type == 'manual': return "Manual only" else: return schedule_type except: return str(schedule_config)[:50] def _on_selection_changed(self): """Handle selection change""" has_selection = len(self.table.selectedItems()) > 0 self.edit_schedule_btn.setEnabled(has_selection) self.delete_schedule_btn.setEnabled(has_selection) def _get_selected_schedule_id(self) -> int: """Get selected schedule ID""" selected_rows = self.table.selectionModel().selectedRows() if not selected_rows: return None row = selected_rows[0].row() return int(self.table.item(row, 0).text()) def _new_schedule(self): """Create new schedule""" dialog = ScheduleEditorDialog(self, self.db, self.config) if dialog.exec(): self.refresh() def _edit_schedule(self): """Edit selected schedule""" schedule_id = self._get_selected_schedule_id() if not schedule_id: return dialog = ScheduleEditorDialog(self, self.db, self.config, schedule_id=schedule_id) if dialog.exec(): self.refresh() def _delete_schedule(self): """Delete selected schedule""" schedule_id = self._get_selected_schedule_id() if not schedule_id: return try: schedule = self.db.get_schedule(schedule_id) reply = QMessageBox.question( self, "Confirm Delete", f"Delete schedule '{schedule['name']}'?\n\n(The target {schedule['target_type']} will not be deleted)", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self.db.delete_schedule(schedule_id) logger.info(f"Deleted schedule {schedule_id}") QMessageBox.information(self, "Success", f"Schedule '{schedule['name']}' deleted") self.refresh() except Exception as e: logger.error(f"Error deleting schedule: {e}") QMessageBox.critical(self, "Error", f"Failed to delete schedule: {e}")