# C:\PYTHON\MyICR_Suite\app\features\dash_diritti\__init__.py """ Dashboard Analisi Diritti - Refactored to use BaseDashboard """ import pandas as pd from PyQt6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QPushButton from PyQt6.QtGui import QIcon from PyQt6.QtCore import QSize, QTimer from loguru import logger from app.features.base_dashboard import BaseDashboard from .data_loaders import DirittiDataLoader from app.widgets.loading_overlay import LoadingOverlay from .data_loaders import DirittiDetailDataLoader from .pivot_table_diritti_widget import PivotTableDirittiWidget from .filter_panel_widget import FilterPanelDiritti from .detail_table_widget import DetailTableDirittiWidget class DashboardDirittiPage(BaseDashboard): """ Dashboard per analisi diritti con pivot table. Refactored per usare BaseDashboard - elimina duplicazione codice. """ def __init__(self, main_window, filter_data=None): super().__init__(main_window, filter_data) # Attributi specifici di questo dashboard self.current_selection = {} self.init_ui(filter_data) # ========== IMPLEMENTAZIONE METODI RICHIESTI DA BaseDashboard ========== def get_dashboard_title(self) -> str: """Titolo del dashboard""" return "Dashboard Analisi Diritti" def get_export_filename(self) -> str: """Nome file di default per export""" sel = self.current_selection safe_name = f"{sel.get('tipologia', 'dettaglio').replace(' ', '_')}_{sel.get('anno', '')}_{sel.get('fr_rr', '')}" return f"dettaglio_diritti_{safe_name}" # ========== UI INITIALIZATION ========== def init_ui(self, filter_data): """Inizializza l'interfaccia utente""" page_layout = QHBoxLayout(self) page_layout.setContentsMargins(0, 0, 0, 0) page_layout.setSpacing(0) # Filter panel (sinistra) self.filter_panel = FilterPanelDiritti(filter_data=filter_data, theme=self.theme) filter_container = QFrame() filter_container.setFixedWidth(280) filter_container.setStyleSheet( f"background-color: {self.theme.get('background_panel', '#F3F2F1')}; " f"border-right: 1px solid #D2D0CE;" ) filter_layout = QVBoxLayout(filter_container) filter_layout.addWidget(self.filter_panel) page_layout.addWidget(filter_container) # Content area (destra) content_widget = QFrame() main_layout = QVBoxLayout(content_widget) main_layout.setContentsMargins(16, 16, 16, 16) main_layout.setSpacing(12) # Header header_layout = QHBoxLayout() header_label = QLabel(self.get_dashboard_title()) header_label.setStyleSheet("font-size: 20px; font-weight: 600;") header_layout.addWidget(header_label) header_layout.addStretch() # Pulsante Aggiorna refresh_btn = QPushButton("🔄 Aggiorna") refresh_btn.setToolTip("Ricarica dati dal database (ignora cache)") refresh_btn.clicked.connect(self.refresh_data_with_cache_clear) header_layout.addWidget(refresh_btn) main_layout.addLayout(header_layout) # Pivot table container pivot_container = QFrame() pivot_container.setFrameShape(QFrame.Shape.NoFrame) pivot_layout = QVBoxLayout(pivot_container) pivot_layout.setContentsMargins(0, 0, 0, 0) self.pivot_table = PivotTableDirittiWidget(theme=self.theme) pivot_layout.addWidget(self.pivot_table) main_layout.addWidget(pivot_container, 2) self.loading_overlay = LoadingOverlay(pivot_container) # Detail toolbar detail_toolbar_layout = QHBoxLayout() detail_toolbar_layout.setContentsMargins(0, 0, 0, 0) detail_label = QLabel("Dettaglio Diritti") detail_label.setStyleSheet("font-size: 16px; font-weight: 600; margin-top: 10px;") detail_toolbar_layout.addWidget(detail_label) detail_toolbar_layout.addStretch() # Action buttons - usa metodo di BaseDashboard con override per stile custom self.reset_button = self._create_action_button_custom("🔄", "Ripristina vista") self.reset_button.clicked.connect(self.reset_selection) self.fullscreen_button = self._create_action_button_custom("⛶", "Vista a schermo intero") self.fullscreen_button.clicked.connect(self.open_detail_fullscreen) self.export_button = self._create_action_button_custom("", "Esporta in Excel") self.export_button.clicked.connect(self.export_detail_to_excel) # Icona Excel personalizzata try: from app.utils.paths import resource_path icon_path = resource_path("assets/icons/excel.svg") self.export_button.setIcon(QIcon(str(icon_path))) self.export_button.setIconSize(QSize(28, 28)) self.export_button.setFixedSize(36, 36) except Exception: self.export_button.setText("📗") detail_toolbar_layout.addWidget(self.reset_button) detail_toolbar_layout.addWidget(self.fullscreen_button) detail_toolbar_layout.addWidget(self.export_button) main_layout.addLayout(detail_toolbar_layout) # Detail table self.detail_table = DetailTableDirittiWidget() self.detail_table.set_theme(self.theme) main_layout.addWidget(self.detail_table, 1) page_layout.addWidget(content_widget, 1) # Abilita keyboard shortcuts self.setup_keyboard_shortcuts() # Signal connections self.filter_panel.filters_applied.connect(self.load_pivot_data) self.pivot_table.cell_selected_for_detail.connect(self.load_detail_data) def _create_action_button_custom(self, icon, tip): """ Crea action button con stile custom per questo dashboard. Override del metodo base per mantenere lo stile minimalista originale. """ from PyQt6.QtWidgets import QPushButton button = QPushButton(icon) button.setToolTip(tip) button.setFixedSize(28, 28) button.setStyleSheet( "QPushButton { font-size: 16px; border: none; } " "QPushButton:hover { color: #0078D4; }" ) button.setVisible(False) return button # ========== DATA LOADING ========== def load_pivot_data(self, filters=None): """Carica dati pivot con filtri applicati""" self.current_filters = filters or {} QTimer.singleShot(0, self._start_background_load) def _start_background_load(self): """Avvia caricamento in background""" # Cleanup thread esistente usando metodo sicuro di BaseDashboard if self.data_thread and self.data_thread.isRunning(): from app.utils.cancelable_thread import safe_terminate_thread safe_terminate_thread(self.data_thread, wait_ms=1000, force=True) self.reset_selection() self.loading_overlay.start_loading() self.data_thread = DirittiDataLoader(self.current_filters) self.data_thread.data_loaded.connect(self.on_pivot_data_loaded) self.data_thread.error_occurred.connect(self.on_data_error) self.data_thread.start() def on_pivot_data_loaded(self, pivot_data: pd.DataFrame): """Callback quando dati pivot sono caricati""" self.loading_overlay.stop_loading() if pivot_data.empty: logger.warning("Nessun dato per la pivot ricevuto dal worker.") self.pivot_table.clear_table() else: self.pivot_table.populate( pivot_data, active_fr_rr_filter=self.current_filters.get('fr_rr_filter') ) def on_data_error(self, error_msg: str): """Callback errore caricamento dati""" self.loading_overlay.stop_loading() QMessageBox.critical( self, "Errore Caricamento Dati", f"Si è verificato un errore:\n\n{error_msg}" ) def load_detail_data(self, tipologia, anno, fr_rr): """Carica dati di dettaglio per la cella selezionata in modo asincrono""" self.current_selection = { 'tipologia': tipologia, 'anno': anno, 'fr_rr': fr_rr } # Pulisci tabella e mostra stato di caricamento self.detail_table.clear_data() self.reset_button.setVisible(True) self.fullscreen_button.setVisible(False) self.export_button.setVisible(False) # Termina thread dettagli precedente se in esecuzione if self.detail_thread and self.detail_thread.isRunning(): logger.info("Terminazione thread dettagli precedente...") self.detail_thread.cancel() self.detail_thread.wait(1000) # Avvia caricamento asincrono self.detail_thread = DirittiDetailDataLoader( tipologia, str(anno), fr_rr, self.current_filters ) self.detail_thread.detail_loaded.connect(self.on_detail_loaded) self.detail_thread.error_occurred.connect(self.on_detail_error) self.detail_thread.start() def on_detail_loaded(self, detail_df): """Callback quando i dati di dettaglio sono caricati""" self.detail_table.populate(detail_df) has_data = not detail_df.empty self.fullscreen_button.setVisible(has_data) self.export_button.setVisible(has_data) def on_detail_error(self, error_msg): """Callback errore caricamento dettagli""" QMessageBox.critical( self, "Errore Caricamento Dettagli", f"Si è verificato un errore nel caricamento dei dettagli:\n\n{error_msg}" ) # ========== OVERRIDE METODI BaseDashboard ========== def reset_selection(self): """ Override reset_selection per gestire logica specifica. Nasconde anche i bottoni toolbar. """ self.detail_table.clear_data() self.pivot_table.clearSelection() self.current_selection = {} # Nasconde action buttons self.reset_button.setVisible(False) self.fullscreen_button.setVisible(False) self.export_button.setVisible(False) def export_detail_to_excel(self): """ Override export per gestire formattazione date specifica. """ data_to_export = self.detail_table.original_data if data_to_export is None or data_to_export.empty: QMessageBox.information( self, "Info", "Nessun dato da esportare." ) return from PyQt6.QtWidgets import QFileDialog default_filename = f"{self.get_export_filename()}.xlsx" path, _ = QFileDialog.getSaveFileName( self, "Esporta Dettaglio Diritti", default_filename, "File Excel (*.xlsx)" ) if not path: return try: # Export usando utility con date native e numeri nativi from app.utils.excel_export import export_qtablewidget_to_excel export_qtablewidget_to_excel( self.detail_table, path, sheet_name="Dettaglio", theme=self.theme, original_data=data_to_export, date_columns=['decr', 'scad'], # Colonne da esportare come date native number_columns=['num_episodi', 'durata'] # Colonne da esportare come numeri nativi ) # Status message if hasattr(self.main_window, 'show_status_message'): self.main_window.show_status_message( "✅ Esportato con successo.", True, 4000 ) logger.info(f"Export completato: {path}") except Exception as e: logger.error(f"Errore export Excel: {e}", exc_info=True) QMessageBox.critical( self, "Errore Esportazione", f"Impossibile salvare il file:\n{e}" ) def open_detail_fullscreen(self): """Apre vista fullscreen del dettaglio""" if self.detail_table.original_data is None or self.detail_table.original_data.empty: return sel = self.current_selection title = f"Dettaglio: {sel.get('tipologia')} ({sel.get('anno')})" page_data = { 'dataframe': self.detail_table.original_data, 'title': title } self.open_new_tab_requested.emit( 'fullscreen_diritti', title, False, page_data ) # ========== LIFECYCLE METHODS ========== def cleanup_threads(self): """ Override per cleanup thread custom di questo dashboard. Chiamato automaticamente da BaseDashboard.page_deactivated() """ # DirittiDataLoader è già gestito da BaseDashboard.data_thread # Nessun thread aggiuntivo da pulire pass def get_cache_namespaces_to_invalidate(self) -> list: """Restituisce i namespace della cache da invalidare""" return ['diritti_raw', 'diritti_detail'] def on_refresh_requested(self): """Ricarica i dati applicando i filtri correnti""" logger.info("Refresh dati richiesto per DashboardDirittiPage") self.filter_panel.apply_filters()