diff options
Diffstat (limited to 'calendar_gui.py')
| -rw-r--r-- | calendar_gui.py | 1406 |
1 files changed, 703 insertions, 703 deletions
diff --git a/calendar_gui.py b/calendar_gui.py index 8e05b37..5705e7a 100644 --- a/calendar_gui.py +++ b/calendar_gui.py @@ -1,704 +1,704 @@ -import sys -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QLineEdit, QPushButton, QDateEdit, QTableWidget, - QTableWidgetItem, QHeaderView, QDialog, QFormLayout, QComboBox, - QMessageBox, QSpinBox, QAction, QFileDialog, QMenuBar, QTextEdit, - QAbstractItemView, QToolButton) -from PyQt5.QtWidgets import QStyle -from PyQt5.QtCore import Qt, QDate, QLocale -from PyQt5.QtGui import QFont, QFontDatabase -from datetime import datetime, timedelta -from dateutil.relativedelta import relativedelta - -from calendar_manager import CalendarManager -from date_calculator import DateCalculator -from prediction_controller import PredictionController -from event_type_handler import EventTypeHandler -from date_service import DateService -from config import EventConfig -from prediction_report_service import PredictionReportService - -class EventDialog(QDialog): - def __init__(self, entry=None, parent=None): - super().__init__(parent) - - self.entry = entry - self.dateformat = EventConfig.DATE_FORMAT - - # Set title based on mode - self.setWindowTitle("Neuer Eintrag" if not entry else "Eintrag editieren") - self.init_ui() - - def init_ui(self): - layout = QFormLayout() - - # Apply Arial font - font = QFont("Arial", 12) - self.setFont(font) - self.setStyleSheet( - "QDialog { background: #ffffff; }" - " QLabel { color: #333; }" - " QLineEdit, QDateEdit, QComboBox, QTextEdit, QSpinBox {" - " background: #fafbfe; color: #333; border: 1px solid #e1e4ee;" - " border-radius: 6px; padding: 6px; }" - " QDateEdit::drop-down, QComboBox::drop-down { border: none; }" - " QPushButton { background: #2f6feb; color: #fff; border: none;" - " border-radius: 6px; padding: 8px 12px; }" - " QPushButton:hover { background: #316de0; }" - " QPushButton:pressed { background: #2a5fcc; }" - ) - - # Start date selector - self.start_date = QDateEdit() - self.start_date.setCalendarPopup(True) - self.start_date.setFont(font) - self.start_date.setDisplayFormat(self.dateformat) - - # Set initial date based on mode - if self.entry: - entry_start = QDate(self.entry.start_date.year, - self.entry.start_date.month, - self.entry.start_date.day) - self.start_date.setDate(entry_start) - else: - self.start_date.setDate(QDate.currentDate()) - - - # Connect start date change to validate end date - self.start_date.dateChanged.connect(self.validate_end_date) - - layout.addRow("Startdatum:", self.start_date) - - # End date selector - self.end_date = QDateEdit() - self.end_date.setCalendarPopup(True) - self.end_date.setFont(font) - self.end_date.setDisplayFormat(self.dateformat) - - # Set initial date based on mode - if self.entry: - entry_end = QDate(self.entry.end_date.year, - self.entry.end_date.month, - self.entry.end_date.day) - self.end_date.setDate(entry_end) - else: - self.end_date.setDate(QDate.currentDate().addDays(1)) - - layout.addRow("Enddatum:", self.end_date) - - # Keyword selector - self.keyword = QComboBox() - self.keyword.setFont(font) - self.keyword.addItems(EventConfig.KEYWORDS) - - # Set initial keyword based on mode - if self.entry and self.entry.keyword in EventConfig.KEYWORDS: - current_index = EventConfig.KEYWORDS.index(self.entry.keyword) - self.keyword.setCurrentIndex(current_index) - - self.keyword.currentTextChanged.connect(self.on_keyword_changed) - layout.addRow("Art:", self.keyword) - - # Commentary input - self.commentary_input = QTextEdit() - self.commentary_input.setFont(font) - self.commentary_input.setFixedHeight(80) - if self.entry and hasattr(self.entry, 'commentary') and self.entry.commentary: - self.commentary_input.setPlainText(self.entry.commentary) - layout.addRow("Kommentar:", self.commentary_input) - - # Store layout for later access - self.layout = layout - self.end_date_row = 1 # Index of end date row in the form layout - - # Buttons - button_layout = QHBoxLayout() - self.save_button = QPushButton("Speichern") - self.save_button.setFont(font) - self.save_button.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton)) - self.save_button.clicked.connect(self.accept) - - self.cancel_button = QPushButton("Abbrechen") - self.cancel_button.setFont(font) - self.cancel_button.setIcon(self.style().standardIcon(QStyle.SP_DialogCancelButton)) - self.cancel_button.clicked.connect(self.reject) - - button_layout.addWidget(self.save_button) - button_layout.addWidget(self.cancel_button) - layout.addRow("", button_layout) - - self.setLayout(layout) - - # Connect end date change to validate start date (keep range consistent both ways) - self.end_date.dateChanged.connect(self.validate_start_date) - - # Handle initial keyword selection - self.on_keyword_changed(self.keyword.currentText()) - - def on_keyword_changed(self, keyword): - """Handle visibility of the end_date field based on keyword""" - # Get the widgets from the form layout - end_date_label = self.layout.itemAt(self.end_date_row, QFormLayout.LabelRole).widget() - end_date_field = self.layout.itemAt(self.end_date_row, QFormLayout.FieldRole).widget() - - if EventTypeHandler.should_hide_end_date_input(keyword): - # Hide end date field for EZ pauschals since they have fixed duration - end_date_label.setVisible(False) - end_date_field.setVisible(False) - - # Update end date automatically if changing to EZ pauschal - start_dt = self.start_date.date().toPyDate() - end_dt = EventTypeHandler.get_duration_for_type(keyword, start_dt) - self.end_date.setDate(QDate(end_dt.year, end_dt.month, end_dt.day)) - else: - # Show end date field for other event types - end_date_label.setVisible(True) - end_date_field.setVisible(True) - - def validate_end_date(self): - """Ensure end date is not before start date""" - if self.end_date.date() < self.start_date.date(): - self.end_date.setDate(self.start_date.date()) - - def validate_start_date(self): - """Ensure start date is not after end date when end changes""" - if self.start_date.date() > self.end_date.date(): - self.start_date.setDate(self.end_date.date()) - def get_data(self): - start_date = DateService.format_date_for_iso(self.start_date.date()) - keyword = self.keyword.currentText() - commentary = self.commentary_input.toPlainText().strip() - - if EventTypeHandler.should_hide_end_date_input(keyword): - # For EZ pauschals, calculate end date automatically - start_dt = DateService.parse_date_from_string(start_date) - end_dt = EventTypeHandler.get_duration_for_type(keyword, start_dt) - end_date = DateService.format_date_for_iso(end_dt) - else: - end_date = DateService.format_date_for_iso(self.end_date.date()) - return start_date, end_date, keyword, commentary - - -class CalendarManagerGUI(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Ausfallzeitenrechner") - self.setMinimumSize(800, 600) - - # Set application font - self.app_font = QFont("Arial", 12) - QApplication.setFont(self.app_font) - - # Get system locale for consistent date formatting - self.locale = QLocale.system() - self.dateformat = EventConfig.DATE_FORMAT - - # Initialize backend components - self.calendar_manager = CalendarManager() - self.date_calculator = DateCalculator() - self.prediction_controller = PredictionController( - self.calendar_manager, - self.date_calculator - ) - - self.init_ui() - self.create_menus() - - def create_menus(self): - # Create menu bar - menubar = self.menuBar() - menubar.setFont(self.app_font) - menubar.setStyleSheet( - "QMenuBar { background: #ffffff; color: #333; }" - " QMenuBar::item:selected { background: #f0f3fa; }" - " QMenu { background: #ffffff; color: #333; border: 1px solid #e1e4ee; }" - " QMenu::item:selected { background: #e6f0ff; }" - ) - - # File menu - file_menu = menubar.addMenu('Start') - - # Load action - load_action = QAction('Laden', self) - load_action.setShortcut('Ctrl+L') - load_action.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton)) - load_action.triggered.connect(self.load_file) - file_menu.addAction(load_action) - - # Save action - save_action = QAction('Speichern', self) - save_action.setShortcut('Ctrl+S') - save_action.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton)) - save_action.triggered.connect(self.save_file) - file_menu.addAction(save_action) - - # Export PDF action - export_pdf_action = QAction('Als PDF exportieren', self) - export_pdf_action.setShortcut('Ctrl+E') - # Fallback icon; SP_DialogPrintButton may not exist in some Qt styles - export_pdf_action.setIcon(self.style().standardIcon(QStyle.SP_FileIcon)) - export_pdf_action.triggered.connect(self.export_pdf) - file_menu.addAction(export_pdf_action) - - # Note: Saving/Loading now operate on complete prediction state - - # Clear action - clear_action = QAction('Einträge löschen', self) - clear_action.setShortcut('Ctrl+N') - clear_action.setIcon(self.style().standardIcon(QStyle.SP_DialogResetButton)) - clear_action.triggered.connect(self.clear_entries) - file_menu.addAction(clear_action) - - # Exit action - exit_action = QAction('Beenden', self) - exit_action.setShortcut('Ctrl+Q') - exit_action.setIcon(self.style().standardIcon(QStyle.SP_DialogCloseButton)) - exit_action.triggered.connect(self.close) - file_menu.addAction(exit_action) - - def init_ui(self): - central_widget = QWidget() - main_layout = QVBoxLayout() - main_layout.setContentsMargins(16, 16, 16, 16) - main_layout.setSpacing(14) - # Global light style for consistency - self.setStyleSheet( - "QMainWindow { background: #ffffff; }" - " QLabel { color: #333; }" - " QLineEdit, QDateEdit, QComboBox, QTextEdit, QSpinBox {" - " background: #fafbfe; color: #333; border: 1px solid #e1e4ee;" - " border-radius: 6px; padding: 6px; }" - " QDateEdit::drop-down, QComboBox::drop-down { border: none; }" - " QPushButton { background: #2f6feb; color: #fff; border: none;" - " border-radius: 6px; padding: 8px 12px; }" - " QPushButton:hover { background: #316de0; }" - " QPushButton:pressed { background: #2a5fcc; }" - " QToolButton { border: none; }" - ) - - top_layout = QHBoxLayout() - top_layout.setSpacing(18) - - # Launch date input - launch_date_layout = QVBoxLayout() - launch_date_label = QLabel("Promotionsdatum:") - launch_date_label.setFont(self.app_font) - self.launch_date_edit = QDateEdit() - self.launch_date_edit.setFont(self.app_font) - self.launch_date_edit.setCalendarPopup(True) # Enable calendar popup - self.launch_date_edit.setDate(QDate.currentDate()) - self.launch_date_edit.setDisplayFormat(self.dateformat) - self.launch_date_edit.dateChanged.connect(self.update_prediction) # Auto-update on change - launch_date_layout.addWidget(launch_date_label) - launch_date_layout.addWidget(self.launch_date_edit) - top_layout.addLayout(launch_date_layout) - - # Duration input - duration_layout = QVBoxLayout() - duration_label = QLabel("Bewerbungszeitraum (Jahre):") - duration_label.setFont(self.app_font) - self.duration_spin = QSpinBox() - self.duration_spin.setFont(self.app_font) - self.duration_spin.setRange(1, 99) - self.duration_spin.setValue(1) - self.duration_spin.valueChanged.connect(self.update_prediction) # Auto-update on change - duration_layout.addWidget(duration_label) - duration_layout.addWidget(self.duration_spin) - top_layout.addLayout(duration_layout) - - # Prediction result - prediction_result_layout = QVBoxLayout() - prediction_result_label = QLabel("Bewerbungsfrist:") - prediction_result_label.setFont(self.app_font) - self.prediction_result = QDateEdit() - self.prediction_result.setFont(self.app_font) - self.prediction_result.setReadOnly(True) - self.prediction_result.setButtonSymbols(QDateEdit.ButtonSymbols.NoButtons) - self.prediction_result.setDisplayFormat(self.dateformat) - self.prediction_result.setStyleSheet( - "QDateEdit { background: #f5faf5; border: 1px solid #cfe8cf; color: #1e4620; }" - ) - prediction_result_layout.addWidget(prediction_result_label) - prediction_result_layout.addWidget(self.prediction_result) - - top_layout.addLayout(prediction_result_layout) - - main_layout.addLayout(top_layout) - - # Events section - events_layout = QVBoxLayout() - events_title = QLabel("<h3>Ausfallzeiten</h3>") - events_title.setFont(self.app_font) - events_layout.addWidget(events_title) - - # Add event button (modern with icon) - add_event_button = QPushButton("Eintrag hinzufügen") - add_event_button.setFont(self.app_font) - add_event_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder)) - add_event_button.setStyleSheet("QPushButton { border-radius: 6px; padding: 6px 10px; }") - add_event_button.clicked.connect(self.add_event) - events_layout.addWidget(add_event_button) - - # Events table - self.events_table = QTableWidget() - self.events_table.setFont(self.app_font) - self.events_table.setColumnCount(7) # ID (hidden), Start, End, Keyword, RelevantTime, Commentary, Actions - self.events_table.setHorizontalHeaderLabels(["ID", "Anfangsdatum", "Enddatum", "Art", "Anger. Zeit", "Kommentar", "Aktionen"]) - self.events_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.events_table.horizontalHeader().setFont(self.app_font) - self.events_table.setColumnHidden(0, True) # Hide ID column - # Table visual tweaks for a modern look - self.events_table.setAlternatingRowColors(True) - self.events_table.setSelectionBehavior(QAbstractItemView.SelectRows) - self.events_table.setSelectionMode(QAbstractItemView.SingleSelection) - self.events_table.setStyleSheet( - "QHeaderView::section { background: #f5f7fb; color: #333; border: none;" - " border-bottom: 1px solid #e6e8ef; padding: 8px; }" - " QTableWidget { gridline-color: #e6e8ef; }" - " QTableView::item { padding: 6px; color: #333; }" - " QTableView::item:selected { background: #e6f0ff; color: #333; }" - " QTableView::item:hover { background: #f4f7ff; color: #333; }" - ) - # Wrap long text and auto-resize rows so comments are readable - self.events_table.setWordWrap(True) - self.events_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) - # Enable inline editing on double click / Enter; keep computed column read-only in population step - self._suppress_item_changed = False - self.events_table.setEditTriggers( - QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked | QAbstractItemView.EditKeyPressed - ) - self.events_table.itemChanged.connect(self.on_events_item_changed) - events_layout.addWidget(self.events_table) - - main_layout.addLayout(events_layout) - - # Set main layout - central_widget.setLayout(main_layout) - self.setCentralWidget(central_widget) - - # Load initial data - self.update_events_table() - self.update_prediction() # Calculate initial prediction - - def update_prediction(self): - """Update prediction whenever needed""" - try: - launch_date = self.launch_date_edit.date().toString("yyyy-MM-dd") - duration_years = self.duration_spin.value() - - self.prediction_controller.make_prediction(launch_date, duration_years) - prediction_date = self.prediction_controller.get_prediction() - - if prediction_date: - self.prediction_result.setDate(prediction_date) - - # Update table to show corrected dates - self.update_events_table() - - except Exception as e: - self.prediction_result.setText("Error in calculation") - print(f"Error calculating prediction: {str(e)}") - - def add_event(self): - dialog = EventDialog(parent=self) - if dialog.exec_(): - start_date, end_date, keyword, commentary = dialog.get_data() - try: - # Validate data before adding - errors = self.calendar_manager.validate_entry_data(start_date, end_date, keyword) - if errors: - QMessageBox.warning(self, "Validation Error", "\n".join(errors)) - return - - self.calendar_manager.add_entry(start_date, end_date, keyword, commentary) - self.update_events_table() - self.update_prediction() # Auto-update prediction - except Exception as e: - QMessageBox.critical(self, "Error", f"Error adding event: {str(e)}") - - def modify_event(self, event_id): - entry = self.calendar_manager.get_entry_by_id(event_id) - if entry: - dialog = EventDialog(entry=entry, parent=self) - if dialog.exec_(): - start_date, end_date, keyword, commentary = dialog.get_data() - try: - # Validate data before modifying - errors = self.calendar_manager.validate_entry_data(start_date, end_date, keyword) - if errors: - QMessageBox.warning(self, "Validation Error", "\n".join(errors)) - return - - self.calendar_manager.modify_entry(event_id, start_date, end_date, keyword, commentary) - self.update_events_table() - self.update_prediction() # Auto-update prediction - except Exception as e: - QMessageBox.critical(self, "Error", f"Error modifying event: {str(e)}") - - def delete_event(self, event_id): - reply = QMessageBox.question(self, "Eintrag löschen", - "Wollen Sie diesen Eintrag löschen?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - try: - self.calendar_manager.delete_entry(event_id) - self.update_events_table() - self.update_prediction() # Auto-update prediction - except Exception as e: - QMessageBox.critical(self, "Error", f"Error deleting event: {str(e)}") - - def update_events_table(self): - # Clear table - self._suppress_item_changed = True - try: - self.events_table.setRowCount(0) - - # Add entries to table - entries = self.calendar_manager.list_entries() - for i, entry in enumerate(entries): - self.events_table.insertRow(i) - - # Set item data - self.events_table.setItem(i, 0, QTableWidgetItem(entry.id)) - - # Format dates using unified display format - start_date_text = DateService.format_date_for_display(entry.start_date, self.dateformat) - end_date_text = DateService.format_date_for_display(entry.end_date, self.dateformat) - self.events_table.setItem(i, 1, QTableWidgetItem(start_date_text)) - self.events_table.setItem(i, 2, QTableWidgetItem(end_date_text)) - # Keyword as dropdown (combobox) for safer selection - keyword_combo = QComboBox() - keyword_combo.setFont(self.app_font) - keyword_combo.addItems(EventConfig.KEYWORDS) - if entry.keyword in EventConfig.KEYWORDS: - keyword_combo.setCurrentIndex(EventConfig.KEYWORDS.index(entry.keyword)) - keyword_combo.currentTextChanged.connect(lambda new_kw, eid=entry.id: self.on_keyword_changed_in_table(eid, new_kw)) - self.events_table.setCellWidget(i, 3, keyword_combo) - - # Relevant accounted time from stored time_period - relevant_text = getattr(entry, 'time_period', "") or "" - relevant_item = QTableWidgetItem(relevant_text) - # Make computed column read-only - relevant_item.setFlags(relevant_item.flags() & ~Qt.ItemIsEditable) - self.events_table.setItem(i, 4, relevant_item) - - # Commentary - commentary_text = getattr(entry, 'commentary', "") or "" - commentary_item = QTableWidgetItem(commentary_text) - self.events_table.setItem(i, 5, commentary_item) - - # Action buttons - actions_widget = QWidget() - actions_layout = QHBoxLayout() - actions_layout.setContentsMargins(0, 0, 0, 0) - # Modern icon-only tool buttons - modify_button = QToolButton() - modify_button.setAutoRaise(True) - modify_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView)) - modify_button.setToolTip("Eintrag bearbeiten") - modify_button.setFixedSize(28, 24) - - delete_button = QToolButton() - delete_button.setAutoRaise(True) - delete_button.setIcon(self.style().standardIcon(QStyle.SP_TrashIcon)) - delete_button.setToolTip("Eintrag löschen") - delete_button.setFixedSize(28, 24) - - # Use lambda with default argument to capture the correct event_id - modify_button.clicked.connect(lambda checked, eid=entry.id: self.modify_event(eid)) - delete_button.clicked.connect(lambda checked, eid=entry.id: self.delete_event(eid)) - - actions_layout.addWidget(modify_button) - actions_layout.addWidget(delete_button) - actions_widget.setLayout(actions_layout) - - self.events_table.setCellWidget(i, 6, actions_widget) - # Adjust row heights after population to show wrapped text - self.events_table.resizeRowsToContents() - finally: - self._suppress_item_changed = False - - def on_events_item_changed(self, item): - # Avoid reacting to programmatic changes - if self._suppress_item_changed: - return - row = item.row() - col = item.column() - # Ignore non-editable/computed/action columns - if col in (0, 4, 6): - return - - # Retrieve entry id - id_item = self.events_table.item(row, 0) - if id_item is None: - return - event_id = id_item.text() - - # Collect current row values - start_text = self.events_table.item(row, 1).text() if self.events_table.item(row, 1) else "" - end_text = self.events_table.item(row, 2).text() if self.events_table.item(row, 2) else "" - # Keyword comes from the combobox cell widget - keyword_widget = self.events_table.cellWidget(row, 3) - keyword_text = keyword_widget.currentText() if isinstance(keyword_widget, QComboBox) else (self.events_table.item(row, 3).text() if self.events_table.item(row, 3) else "") - commentary_text = self.events_table.item(row, 5).text() if self.events_table.item(row, 5) else "" - - # Normalize to ISO strings for backend - try: - start_iso = DateService.format_date_for_iso(DateService.parse_date_from_string(start_text)) if start_text else None - end_iso = DateService.format_date_for_iso(DateService.parse_date_from_string(end_text)) if end_text else None - except Exception as e: - QMessageBox.warning(self, "Ungültiges Datum", f"Bitte ein gültiges Datum eingeben.\nFehler: {str(e)}") - # Revert table to backend values - self.update_events_table() - return - - # Validate before applying - errors = [] - if start_iso and end_iso: - errors = self.calendar_manager.validate_entry_data(start_iso, end_iso, keyword_text) - if errors: - QMessageBox.warning(self, "Validierungsfehler", "\n".join(errors)) - self.update_events_table() - return - - # Apply modification - modified = self.calendar_manager.modify_entry( - event_id, - start_date=start_iso if start_iso else None, - end_date=end_iso if end_iso else None, - keyword=keyword_text if keyword_text else None, - commentary=commentary_text if commentary_text is not None else None, - ) - if modified is None: - QMessageBox.critical(self, "Fehler", "Eintrag konnte nicht aktualisiert werden.") - self.update_events_table() - return - - # Refresh to reflect normalized display and recomputed values - self.update_events_table() - self.update_prediction() - - def on_keyword_changed_in_table(self, event_id, new_keyword): - # Validate using current start/end from the entry - entry = self.calendar_manager.get_entry_by_id(event_id) - if not entry: - return - start_iso = DateService.format_date_for_iso(entry.start_date) - end_iso = DateService.format_date_for_iso(entry.end_date) - errors = self.calendar_manager.validate_entry_data(start_iso, end_iso, new_keyword) - if errors: - QMessageBox.warning(self, "Validierungsfehler", "\n".join(errors)) - # Re-render to revert combo to valid value - self.update_events_table() - return - modified = self.calendar_manager.modify_entry(event_id, keyword=new_keyword) - if modified is None: - QMessageBox.critical(self, "Fehler", "Eintrag konnte nicht aktualisiert werden.") - self.update_events_table() - return - self.update_events_table() - self.update_prediction() - - def save_file(self): - """Save the complete prediction state to a .prediction.json file""" - file_path, _ = QFileDialog.getSaveFileName( - self, - "Zustand speichern", - "", - "Prediction State (*.prediction.json)" - ) - if not file_path: - return - - try: - if self.prediction_controller.save_complete_state(file_path): - target = file_path if file_path.endswith('.prediction.json') else file_path + '.prediction.json' - QMessageBox.information(self, "Speichern erfolgreich", f"Komplettzustand gespeichert in {target}") - else: - QMessageBox.critical(self, "Error", "Speichern des Komplettzustands fehlgeschlagen") - except Exception as e: - QMessageBox.critical(self, "Error", f"Fehler beim Speichern des Komplettzustands: {str(e)}") - - def load_file(self): - """Load the complete prediction state from a .prediction.json file""" - file_path, _ = QFileDialog.getOpenFileName( - self, - "Zustand laden", - "", - "Prediction State (*.prediction.json)" - ) - if not file_path: - return - - try: - success = self.prediction_controller.load_complete_state(file_path) - if success: - # Reflect restored parameters into UI - launch_dt = self.prediction_controller.get_launch_date() - duration = self.prediction_controller.get_duration() - if launch_dt: - self.launch_date_edit.setDate(launch_dt) - if duration is not None and hasattr(duration, 'years'): - self.duration_spin.setValue(max(1, duration.years)) - - # Update views - self.update_events_table() - prediction_date = self.prediction_controller.get_prediction() - if prediction_date: - self.prediction_result.setDate(prediction_date) - QMessageBox.information(self, "Laden erfolgreich", f"Komplettzustand von {file_path} geladen") - else: - QMessageBox.warning(self, "Warnung", "Komplettzustand konnte nicht geladen werden") - except Exception as e: - QMessageBox.critical(self, "Error", f"Fehler beim Laden des Komplettzustands: {str(e)}") - - def export_pdf(self): - """Export a formatted report to PDF including metadata and full table.""" - out_path, _ = QFileDialog.getSaveFileName( - self, - "PDF exportieren", - "", - "PDF (*.pdf)" - ) - if not out_path: - return - success = PredictionReportService.export_pdf( - self.calendar_manager, - self.prediction_controller, - out_path, - self.dateformat, - ) - if success: - if not out_path.lower().endswith('.pdf'): - out_path = out_path + '.pdf' - QMessageBox.information(self, "Export erfolgreich", f"PDF exportiert nach {out_path}") - else: - QMessageBox.critical(self, "Fehler", "PDF-Export fehlgeschlagen.") - - def clear_entries(self): - """Clear all calendar entries""" - reply = QMessageBox.question(self, "Einträge löschen", - "Alle Einträge löschen?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - try: - self.calendar_manager.clear_entries() - self.update_events_table() - self.update_prediction() # Auto-update prediction - QMessageBox.information(self, "Erfolg", "Alle Einträge gelöscht!") - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to clear calendar: {str(e)}") - - -def main(): - app = QApplication(sys.argv) - app.setStyle('Fusion') - # Ensure Arial is available - QFontDatabase.addApplicationFont("Arial") - window = CalendarManagerGUI() - window.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": +import sys
+from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+ QLabel, QLineEdit, QPushButton, QDateEdit, QTableWidget,
+ QTableWidgetItem, QHeaderView, QDialog, QFormLayout, QComboBox,
+ QMessageBox, QSpinBox, QAction, QFileDialog, QMenuBar, QTextEdit,
+ QAbstractItemView, QToolButton)
+from PyQt5.QtWidgets import QStyle
+from PyQt5.QtCore import Qt, QDate, QLocale
+from PyQt5.QtGui import QFont, QFontDatabase
+from datetime import datetime, timedelta
+from dateutil.relativedelta import relativedelta
+
+from calendar_manager import CalendarManager
+from date_calculator import DateCalculator
+from prediction_controller import PredictionController
+from event_type_handler import EventTypeHandler
+from date_service import DateService
+from config import EventConfig
+from prediction_report_service import PredictionReportService
+
+class EventDialog(QDialog):
+ def __init__(self, entry=None, parent=None):
+ super().__init__(parent)
+
+ self.entry = entry
+ self.dateformat = EventConfig.DATE_FORMAT
+
+ # Set title based on mode
+ self.setWindowTitle("Neuer Eintrag" if not entry else "Eintrag editieren")
+ self.init_ui()
+
+ def init_ui(self):
+ layout = QFormLayout()
+
+ # Apply Arial font
+ font = QFont("Arial", 12)
+ self.setFont(font)
+ self.setStyleSheet(
+ "QDialog { background: #ffffff; }"
+ " QLabel { color: #333; }"
+ " QLineEdit, QDateEdit, QComboBox, QTextEdit, QSpinBox {"
+ " background: #fafbfe; color: #333; border: 1px solid #e1e4ee;"
+ " border-radius: 6px; padding: 6px; }"
+ " QDateEdit::drop-down, QComboBox::drop-down { border: none; }"
+ " QPushButton { background: #2f6feb; color: #fff; border: none;"
+ " border-radius: 6px; padding: 8px 12px; }"
+ " QPushButton:hover { background: #316de0; }"
+ " QPushButton:pressed { background: #2a5fcc; }"
+ )
+
+ # Start date selector
+ self.start_date = QDateEdit()
+ self.start_date.setCalendarPopup(True)
+ self.start_date.setFont(font)
+ self.start_date.setDisplayFormat(self.dateformat)
+
+ # Set initial date based on mode
+ if self.entry:
+ entry_start = QDate(self.entry.start_date.year,
+ self.entry.start_date.month,
+ self.entry.start_date.day)
+ self.start_date.setDate(entry_start)
+ else:
+ self.start_date.setDate(QDate.currentDate())
+
+
+ # Connect start date change to validate end date
+ self.start_date.dateChanged.connect(self.validate_end_date)
+
+ layout.addRow("Startdatum:", self.start_date)
+
+ # End date selector
+ self.end_date = QDateEdit()
+ self.end_date.setCalendarPopup(True)
+ self.end_date.setFont(font)
+ self.end_date.setDisplayFormat(self.dateformat)
+
+ # Set initial date based on mode
+ if self.entry:
+ entry_end = QDate(self.entry.end_date.year,
+ self.entry.end_date.month,
+ self.entry.end_date.day)
+ self.end_date.setDate(entry_end)
+ else:
+ self.end_date.setDate(QDate.currentDate().addDays(1))
+
+ layout.addRow("Enddatum:", self.end_date)
+
+ # Keyword selector
+ self.keyword = QComboBox()
+ self.keyword.setFont(font)
+ self.keyword.addItems(EventConfig.KEYWORDS)
+
+ # Set initial keyword based on mode
+ if self.entry and self.entry.keyword in EventConfig.KEYWORDS:
+ current_index = EventConfig.KEYWORDS.index(self.entry.keyword)
+ self.keyword.setCurrentIndex(current_index)
+
+ self.keyword.currentTextChanged.connect(self.on_keyword_changed)
+ layout.addRow("Art:", self.keyword)
+
+ # Commentary input
+ self.commentary_input = QTextEdit()
+ self.commentary_input.setFont(font)
+ self.commentary_input.setFixedHeight(80)
+ if self.entry and hasattr(self.entry, 'commentary') and self.entry.commentary:
+ self.commentary_input.setPlainText(self.entry.commentary)
+ layout.addRow("Kommentar:", self.commentary_input)
+
+ # Store layout for later access
+ self.layout = layout
+ self.end_date_row = 1 # Index of end date row in the form layout
+
+ # Buttons
+ button_layout = QHBoxLayout()
+ self.save_button = QPushButton("Speichern")
+ self.save_button.setFont(font)
+ self.save_button.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
+ self.save_button.clicked.connect(self.accept)
+
+ self.cancel_button = QPushButton("Abbrechen")
+ self.cancel_button.setFont(font)
+ self.cancel_button.setIcon(self.style().standardIcon(QStyle.SP_DialogCancelButton))
+ self.cancel_button.clicked.connect(self.reject)
+
+ button_layout.addWidget(self.save_button)
+ button_layout.addWidget(self.cancel_button)
+ layout.addRow("", button_layout)
+
+ self.setLayout(layout)
+
+ # Connect end date change to validate start date (keep range consistent both ways)
+ self.end_date.dateChanged.connect(self.validate_start_date)
+
+ # Handle initial keyword selection
+ self.on_keyword_changed(self.keyword.currentText())
+
+ def on_keyword_changed(self, keyword):
+ """Handle visibility of the end_date field based on keyword"""
+ # Get the widgets from the form layout
+ end_date_label = self.layout.itemAt(self.end_date_row, QFormLayout.LabelRole).widget()
+ end_date_field = self.layout.itemAt(self.end_date_row, QFormLayout.FieldRole).widget()
+
+ if EventTypeHandler.should_hide_end_date_input(keyword):
+ # Hide end date field for EZ pauschals since they have fixed duration
+ end_date_label.setVisible(False)
+ end_date_field.setVisible(False)
+
+ # Update end date automatically if changing to EZ pauschal
+ start_dt = self.start_date.date().toPyDate()
+ end_dt = EventTypeHandler.get_duration_for_type(keyword, start_dt)
+ self.end_date.setDate(QDate(end_dt.year, end_dt.month, end_dt.day))
+ else:
+ # Show end date field for other event types
+ end_date_label.setVisible(True)
+ end_date_field.setVisible(True)
+
+ def validate_end_date(self):
+ """Ensure end date is not before start date"""
+ if self.end_date.date() < self.start_date.date():
+ self.end_date.setDate(self.start_date.date())
+
+ def validate_start_date(self):
+ """Ensure start date is not after end date when end changes"""
+ if self.start_date.date() > self.end_date.date():
+ self.start_date.setDate(self.end_date.date())
+ def get_data(self):
+ start_date = DateService.format_date_for_iso(self.start_date.date())
+ keyword = self.keyword.currentText()
+ commentary = self.commentary_input.toPlainText().strip()
+
+ if EventTypeHandler.should_hide_end_date_input(keyword):
+ # For EZ pauschals, calculate end date automatically
+ start_dt = DateService.parse_date_from_string(start_date)
+ end_dt = EventTypeHandler.get_duration_for_type(keyword, start_dt)
+ end_date = DateService.format_date_for_iso(end_dt)
+ else:
+ end_date = DateService.format_date_for_iso(self.end_date.date())
+ return start_date, end_date, keyword, commentary
+
+
+class CalendarManagerGUI(QMainWindow):
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle("Ausfallzeitenrechner")
+ self.setMinimumSize(800, 600)
+
+ # Set application font
+ self.app_font = QFont("Arial", 12)
+ QApplication.setFont(self.app_font)
+
+ # Get system locale for consistent date formatting
+ self.locale = QLocale.system()
+ self.dateformat = EventConfig.DATE_FORMAT
+
+ # Initialize backend components
+ self.calendar_manager = CalendarManager()
+ self.date_calculator = DateCalculator()
+ self.prediction_controller = PredictionController(
+ self.calendar_manager,
+ self.date_calculator
+ )
+
+ self.init_ui()
+ self.create_menus()
+
+ def create_menus(self):
+ # Create menu bar
+ menubar = self.menuBar()
+ menubar.setFont(self.app_font)
+ menubar.setStyleSheet(
+ "QMenuBar { background: #ffffff; color: #333; }"
+ " QMenuBar::item:selected { background: #f0f3fa; }"
+ " QMenu { background: #ffffff; color: #333; border: 1px solid #e1e4ee; }"
+ " QMenu::item:selected { background: #e6f0ff; }"
+ )
+
+ # File menu
+ file_menu = menubar.addMenu('Start')
+
+ # Load action
+ load_action = QAction('Laden', self)
+ load_action.setShortcut('Ctrl+L')
+ load_action.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton))
+ load_action.triggered.connect(self.load_file)
+ file_menu.addAction(load_action)
+
+ # Save action
+ save_action = QAction('Speichern', self)
+ save_action.setShortcut('Ctrl+S')
+ save_action.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
+ save_action.triggered.connect(self.save_file)
+ file_menu.addAction(save_action)
+
+ # Export PDF action
+ export_pdf_action = QAction('Als PDF exportieren', self)
+ export_pdf_action.setShortcut('Ctrl+E')
+ # Fallback icon; SP_DialogPrintButton may not exist in some Qt styles
+ export_pdf_action.setIcon(self.style().standardIcon(QStyle.SP_FileIcon))
+ export_pdf_action.triggered.connect(self.export_pdf)
+ file_menu.addAction(export_pdf_action)
+
+ # Note: Saving/Loading now operate on complete prediction state
+
+ # Clear action
+ clear_action = QAction('Einträge löschen', self)
+ clear_action.setShortcut('Ctrl+N')
+ clear_action.setIcon(self.style().standardIcon(QStyle.SP_DialogDiscardButton))
+ clear_action.triggered.connect(self.clear_entries)
+ file_menu.addAction(clear_action)
+
+ # Exit action
+ exit_action = QAction('Beenden', self)
+ exit_action.setShortcut('Ctrl+Q')
+ exit_action.setIcon(self.style().standardIcon(QStyle.SP_DialogCloseButton))
+ exit_action.triggered.connect(self.close)
+ file_menu.addAction(exit_action)
+
+ def init_ui(self):
+ central_widget = QWidget()
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(16, 16, 16, 16)
+ main_layout.setSpacing(14)
+ # Global light style for consistency
+ self.setStyleSheet(
+ "QMainWindow { background: #ffffff; }"
+ " QLabel { color: #333; }"
+ " QLineEdit, QDateEdit, QComboBox, QTextEdit, QSpinBox {"
+ " background: #fafbfe; color: #333; border: 1px solid #e1e4ee;"
+ " border-radius: 6px; padding: 6px; }"
+ " QDateEdit::drop-down, QComboBox::drop-down { border: none; }"
+ " QPushButton { background: #2f6feb; color: #fff; border: none;"
+ " border-radius: 6px; padding: 8px 12px; }"
+ " QPushButton:hover { background: #316de0; }"
+ " QPushButton:pressed { background: #2a5fcc; }"
+ " QToolButton { border: none; }"
+ )
+
+ top_layout = QHBoxLayout()
+ top_layout.setSpacing(18)
+
+ # Launch date input
+ launch_date_layout = QVBoxLayout()
+ launch_date_label = QLabel("Promotionsdatum:")
+ launch_date_label.setFont(self.app_font)
+ self.launch_date_edit = QDateEdit()
+ self.launch_date_edit.setFont(self.app_font)
+ self.launch_date_edit.setCalendarPopup(True) # Enable calendar popup
+ self.launch_date_edit.setDate(QDate.currentDate())
+ self.launch_date_edit.setDisplayFormat(self.dateformat)
+ self.launch_date_edit.dateChanged.connect(self.update_prediction) # Auto-update on change
+ launch_date_layout.addWidget(launch_date_label)
+ launch_date_layout.addWidget(self.launch_date_edit)
+ top_layout.addLayout(launch_date_layout)
+
+ # Duration input
+ duration_layout = QVBoxLayout()
+ duration_label = QLabel("Bewerbungszeitraum (Jahre):")
+ duration_label.setFont(self.app_font)
+ self.duration_spin = QSpinBox()
+ self.duration_spin.setFont(self.app_font)
+ self.duration_spin.setRange(1, 99)
+ self.duration_spin.setValue(1)
+ self.duration_spin.valueChanged.connect(self.update_prediction) # Auto-update on change
+ duration_layout.addWidget(duration_label)
+ duration_layout.addWidget(self.duration_spin)
+ top_layout.addLayout(duration_layout)
+
+ # Prediction result
+ prediction_result_layout = QVBoxLayout()
+ prediction_result_label = QLabel("Bewerbungsfrist:")
+ prediction_result_label.setFont(self.app_font)
+ self.prediction_result = QDateEdit()
+ self.prediction_result.setFont(self.app_font)
+ self.prediction_result.setReadOnly(True)
+ self.prediction_result.setButtonSymbols(QDateEdit.ButtonSymbols.NoButtons)
+ self.prediction_result.setDisplayFormat(self.dateformat)
+ self.prediction_result.setStyleSheet(
+ "QDateEdit { background: #f5faf5; border: 1px solid #cfe8cf; color: #1e4620; }"
+ )
+ prediction_result_layout.addWidget(prediction_result_label)
+ prediction_result_layout.addWidget(self.prediction_result)
+
+ top_layout.addLayout(prediction_result_layout)
+
+ main_layout.addLayout(top_layout)
+
+ # Events section
+ events_layout = QVBoxLayout()
+ events_title = QLabel("<h3>Ausfallzeiten</h3>")
+ events_title.setFont(self.app_font)
+ events_layout.addWidget(events_title)
+
+ # Add event button (modern with icon)
+ add_event_button = QPushButton("Eintrag hinzufügen")
+ add_event_button.setFont(self.app_font)
+ add_event_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder))
+ add_event_button.setStyleSheet("QPushButton { border-radius: 6px; padding: 6px 10px; }")
+ add_event_button.clicked.connect(self.add_event)
+ events_layout.addWidget(add_event_button)
+
+ # Events table
+ self.events_table = QTableWidget()
+ self.events_table.setFont(self.app_font)
+ self.events_table.setColumnCount(7) # ID (hidden), Start, End, Keyword, RelevantTime, Commentary, Actions
+ self.events_table.setHorizontalHeaderLabels(["ID", "Anfangsdatum", "Enddatum", "Art", "Anger. Zeit", "Kommentar", "Aktionen"])
+ self.events_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+ self.events_table.horizontalHeader().setFont(self.app_font)
+ self.events_table.setColumnHidden(0, True) # Hide ID column
+ # Table visual tweaks for a modern look
+ self.events_table.setAlternatingRowColors(True)
+ self.events_table.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.events_table.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.events_table.setStyleSheet(
+ "QHeaderView::section { background: #f5f7fb; color: #333; border: none;"
+ " border-bottom: 1px solid #e6e8ef; padding: 8px; }"
+ " QTableWidget { gridline-color: #e6e8ef; }"
+ " QTableView::item { padding: 6px; color: #333; }"
+ " QTableView::item:selected { background: #e6f0ff; color: #333; }"
+ " QTableView::item:hover { background: #f4f7ff; color: #333; }"
+ )
+ # Wrap long text and auto-resize rows so comments are readable
+ self.events_table.setWordWrap(True)
+ self.events_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
+ # Enable inline editing on double click / Enter; keep computed column read-only in population step
+ self._suppress_item_changed = False
+ self.events_table.setEditTriggers(
+ QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked | QAbstractItemView.EditKeyPressed
+ )
+ self.events_table.itemChanged.connect(self.on_events_item_changed)
+ events_layout.addWidget(self.events_table)
+
+ main_layout.addLayout(events_layout)
+
+ # Set main layout
+ central_widget.setLayout(main_layout)
+ self.setCentralWidget(central_widget)
+
+ # Load initial data
+ self.update_events_table()
+ self.update_prediction() # Calculate initial prediction
+
+ def update_prediction(self):
+ """Update prediction whenever needed"""
+ try:
+ launch_date = self.launch_date_edit.date().toString("yyyy-MM-dd")
+ duration_years = self.duration_spin.value()
+
+ self.prediction_controller.make_prediction(launch_date, duration_years)
+ prediction_date = self.prediction_controller.get_prediction()
+
+ if prediction_date:
+ self.prediction_result.setDate(prediction_date)
+
+ # Update table to show corrected dates
+ self.update_events_table()
+
+ except Exception as e:
+ self.prediction_result.setText("Error in calculation")
+ print(f"Error calculating prediction: {str(e)}")
+
+ def add_event(self):
+ dialog = EventDialog(parent=self)
+ if dialog.exec_():
+ start_date, end_date, keyword, commentary = dialog.get_data()
+ try:
+ # Validate data before adding
+ errors = self.calendar_manager.validate_entry_data(start_date, end_date, keyword)
+ if errors:
+ QMessageBox.warning(self, "Validation Error", "\n".join(errors))
+ return
+
+ self.calendar_manager.add_entry(start_date, end_date, keyword, commentary)
+ self.update_events_table()
+ self.update_prediction() # Auto-update prediction
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Error adding event: {str(e)}")
+
+ def modify_event(self, event_id):
+ entry = self.calendar_manager.get_entry_by_id(event_id)
+ if entry:
+ dialog = EventDialog(entry=entry, parent=self)
+ if dialog.exec_():
+ start_date, end_date, keyword, commentary = dialog.get_data()
+ try:
+ # Validate data before modifying
+ errors = self.calendar_manager.validate_entry_data(start_date, end_date, keyword)
+ if errors:
+ QMessageBox.warning(self, "Validation Error", "\n".join(errors))
+ return
+
+ self.calendar_manager.modify_entry(event_id, start_date, end_date, keyword, commentary)
+ self.update_events_table()
+ self.update_prediction() # Auto-update prediction
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Error modifying event: {str(e)}")
+
+ def delete_event(self, event_id):
+ reply = QMessageBox.question(self, "Eintrag löschen",
+ "Wollen Sie diesen Eintrag löschen?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ try:
+ self.calendar_manager.delete_entry(event_id)
+ self.update_events_table()
+ self.update_prediction() # Auto-update prediction
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Error deleting event: {str(e)}")
+
+ def update_events_table(self):
+ # Clear table
+ self._suppress_item_changed = True
+ try:
+ self.events_table.setRowCount(0)
+
+ # Add entries to table
+ entries = self.calendar_manager.list_entries()
+ for i, entry in enumerate(entries):
+ self.events_table.insertRow(i)
+
+ # Set item data
+ self.events_table.setItem(i, 0, QTableWidgetItem(entry.id))
+
+ # Format dates using unified display format
+ start_date_text = DateService.format_date_for_display(entry.start_date, self.dateformat)
+ end_date_text = DateService.format_date_for_display(entry.end_date, self.dateformat)
+ self.events_table.setItem(i, 1, QTableWidgetItem(start_date_text))
+ self.events_table.setItem(i, 2, QTableWidgetItem(end_date_text))
+ # Keyword as dropdown (combobox) for safer selection
+ keyword_combo = QComboBox()
+ keyword_combo.setFont(self.app_font)
+ keyword_combo.addItems(EventConfig.KEYWORDS)
+ if entry.keyword in EventConfig.KEYWORDS:
+ keyword_combo.setCurrentIndex(EventConfig.KEYWORDS.index(entry.keyword))
+ keyword_combo.currentTextChanged.connect(lambda new_kw, eid=entry.id: self.on_keyword_changed_in_table(eid, new_kw))
+ self.events_table.setCellWidget(i, 3, keyword_combo)
+
+ # Relevant accounted time from stored time_period
+ relevant_text = getattr(entry, 'time_period', "") or ""
+ relevant_item = QTableWidgetItem(relevant_text)
+ # Make computed column read-only
+ relevant_item.setFlags(relevant_item.flags() & ~Qt.ItemIsEditable)
+ self.events_table.setItem(i, 4, relevant_item)
+
+ # Commentary
+ commentary_text = getattr(entry, 'commentary', "") or ""
+ commentary_item = QTableWidgetItem(commentary_text)
+ self.events_table.setItem(i, 5, commentary_item)
+
+ # Action buttons
+ actions_widget = QWidget()
+ actions_layout = QHBoxLayout()
+ actions_layout.setContentsMargins(0, 0, 0, 0)
+ # Modern icon-only tool buttons
+ modify_button = QToolButton()
+ modify_button.setAutoRaise(True)
+ modify_button.setIcon(self.style().standardIcon(QStyle.SP_DialogResetButton))
+ modify_button.setToolTip("Eintrag bearbeiten")
+ modify_button.setFixedSize(28, 24)
+
+ delete_button = QToolButton()
+ delete_button.setAutoRaise(True)
+ delete_button.setIcon(self.style().standardIcon(QStyle.SP_DialogDiscardButton))
+ delete_button.setToolTip("Eintrag löschen")
+ delete_button.setFixedSize(28, 24)
+
+ # Use lambda with default argument to capture the correct event_id
+ modify_button.clicked.connect(lambda checked, eid=entry.id: self.modify_event(eid))
+ delete_button.clicked.connect(lambda checked, eid=entry.id: self.delete_event(eid))
+
+ actions_layout.addWidget(modify_button)
+ actions_layout.addWidget(delete_button)
+ actions_widget.setLayout(actions_layout)
+
+ self.events_table.setCellWidget(i, 6, actions_widget)
+ # Adjust row heights after population to show wrapped text
+ self.events_table.resizeRowsToContents()
+ finally:
+ self._suppress_item_changed = False
+
+ def on_events_item_changed(self, item):
+ # Avoid reacting to programmatic changes
+ if self._suppress_item_changed:
+ return
+ row = item.row()
+ col = item.column()
+ # Ignore non-editable/computed/action columns
+ if col in (0, 4, 6):
+ return
+
+ # Retrieve entry id
+ id_item = self.events_table.item(row, 0)
+ if id_item is None:
+ return
+ event_id = id_item.text()
+
+ # Collect current row values
+ start_text = self.events_table.item(row, 1).text() if self.events_table.item(row, 1) else ""
+ end_text = self.events_table.item(row, 2).text() if self.events_table.item(row, 2) else ""
+ # Keyword comes from the combobox cell widget
+ keyword_widget = self.events_table.cellWidget(row, 3)
+ keyword_text = keyword_widget.currentText() if isinstance(keyword_widget, QComboBox) else (self.events_table.item(row, 3).text() if self.events_table.item(row, 3) else "")
+ commentary_text = self.events_table.item(row, 5).text() if self.events_table.item(row, 5) else ""
+
+ # Normalize to ISO strings for backend
+ try:
+ start_iso = DateService.format_date_for_iso(DateService.parse_date_from_string(start_text)) if start_text else None
+ end_iso = DateService.format_date_for_iso(DateService.parse_date_from_string(end_text)) if end_text else None
+ except Exception as e:
+ QMessageBox.warning(self, "Ungültiges Datum", f"Bitte ein gültiges Datum eingeben.\nFehler: {str(e)}")
+ # Revert table to backend values
+ self.update_events_table()
+ return
+
+ # Validate before applying
+ errors = []
+ if start_iso and end_iso:
+ errors = self.calendar_manager.validate_entry_data(start_iso, end_iso, keyword_text)
+ if errors:
+ QMessageBox.warning(self, "Validierungsfehler", "\n".join(errors))
+ self.update_events_table()
+ return
+
+ # Apply modification
+ modified = self.calendar_manager.modify_entry(
+ event_id,
+ start_date=start_iso if start_iso else None,
+ end_date=end_iso if end_iso else None,
+ keyword=keyword_text if keyword_text else None,
+ commentary=commentary_text if commentary_text is not None else None,
+ )
+ if modified is None:
+ QMessageBox.critical(self, "Fehler", "Eintrag konnte nicht aktualisiert werden.")
+ self.update_events_table()
+ return
+
+ # Refresh to reflect normalized display and recomputed values
+ self.update_events_table()
+ self.update_prediction()
+
+ def on_keyword_changed_in_table(self, event_id, new_keyword):
+ # Validate using current start/end from the entry
+ entry = self.calendar_manager.get_entry_by_id(event_id)
+ if not entry:
+ return
+ start_iso = DateService.format_date_for_iso(entry.start_date)
+ end_iso = DateService.format_date_for_iso(entry.end_date)
+ errors = self.calendar_manager.validate_entry_data(start_iso, end_iso, new_keyword)
+ if errors:
+ QMessageBox.warning(self, "Validierungsfehler", "\n".join(errors))
+ # Re-render to revert combo to valid value
+ self.update_events_table()
+ return
+ modified = self.calendar_manager.modify_entry(event_id, keyword=new_keyword)
+ if modified is None:
+ QMessageBox.critical(self, "Fehler", "Eintrag konnte nicht aktualisiert werden.")
+ self.update_events_table()
+ return
+ self.update_events_table()
+ self.update_prediction()
+
+ def save_file(self):
+ """Save the complete prediction state to a .prediction.json file"""
+ file_path, _ = QFileDialog.getSaveFileName(
+ self,
+ "Zustand speichern",
+ "",
+ "Prediction State (*.prediction.json)"
+ )
+ if not file_path:
+ return
+
+ try:
+ if self.prediction_controller.save_complete_state(file_path):
+ target = file_path if file_path.endswith('.prediction.json') else file_path + '.prediction.json'
+ QMessageBox.information(self, "Speichern erfolgreich", f"Komplettzustand gespeichert in {target}")
+ else:
+ QMessageBox.critical(self, "Error", "Speichern des Komplettzustands fehlgeschlagen")
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Fehler beim Speichern des Komplettzustands: {str(e)}")
+
+ def load_file(self):
+ """Load the complete prediction state from a .prediction.json file"""
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "Zustand laden",
+ "",
+ "Prediction State (*.prediction.json)"
+ )
+ if not file_path:
+ return
+
+ try:
+ success = self.prediction_controller.load_complete_state(file_path)
+ if success:
+ # Reflect restored parameters into UI
+ launch_dt = self.prediction_controller.get_launch_date()
+ duration = self.prediction_controller.get_duration()
+ if launch_dt:
+ self.launch_date_edit.setDate(launch_dt)
+ if duration is not None and hasattr(duration, 'years'):
+ self.duration_spin.setValue(max(1, duration.years))
+
+ # Update views
+ self.update_events_table()
+ prediction_date = self.prediction_controller.get_prediction()
+ if prediction_date:
+ self.prediction_result.setDate(prediction_date)
+ QMessageBox.information(self, "Laden erfolgreich", f"Komplettzustand von {file_path} geladen")
+ else:
+ QMessageBox.warning(self, "Warnung", "Komplettzustand konnte nicht geladen werden")
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Fehler beim Laden des Komplettzustands: {str(e)}")
+
+ def export_pdf(self):
+ """Export a formatted report to PDF including metadata and full table."""
+ out_path, _ = QFileDialog.getSaveFileName(
+ self,
+ "PDF exportieren",
+ "",
+ "PDF (*.pdf)"
+ )
+ if not out_path:
+ return
+ success = PredictionReportService.export_pdf(
+ self.calendar_manager,
+ self.prediction_controller,
+ out_path,
+ self.dateformat,
+ )
+ if success:
+ if not out_path.lower().endswith('.pdf'):
+ out_path = out_path + '.pdf'
+ QMessageBox.information(self, "Export erfolgreich", f"PDF exportiert nach {out_path}")
+ else:
+ QMessageBox.critical(self, "Fehler", "PDF-Export fehlgeschlagen.")
+
+ def clear_entries(self):
+ """Clear all calendar entries"""
+ reply = QMessageBox.question(self, "Einträge löschen",
+ "Alle Einträge löschen?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ try:
+ self.calendar_manager.clear_entries()
+ self.update_events_table()
+ self.update_prediction() # Auto-update prediction
+ QMessageBox.information(self, "Erfolg", "Alle Einträge gelöscht!")
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Failed to clear calendar: {str(e)}")
+
+
+def main():
+ app = QApplication(sys.argv)
+ app.setStyle('Fusion')
+ # Ensure Arial is available
+ QFontDatabase.addApplicationFont("Arial")
+ window = CalendarManagerGUI()
+ window.show()
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
main()
\ No newline at end of file |
