From 1edb9c91bc1c61904b86d8f0e05527d1af6e50cc Mon Sep 17 00:00:00 2001 From: matin Date: Sat, 4 Oct 2025 17:56:33 +0200 Subject: changes of symbols (cosmetic) --- calendar_gui.py | 1406 +++++++++++++++++++++++++++---------------------------- 1 file 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("

Ausfallzeiten

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

Ausfallzeiten

") + 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 -- cgit v1.1