# --- FILE FINALE CON FORMATTAZIONE NUMERICA CORRETTA --- import logging import pandas as pd from typing import Dict, Optional from datetime import datetime from PyQt6.QtWidgets import ( QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QFrame, QMessageBox ) from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont, QColor logger = logging.getLogger(__name__) # --- Funzione di formattazione aggiornata --- def safe_format_value(value, value_type="text"): if pd.isna(value) or value is None: return "" try: if value_type == "int_dot_sep": return f"{int(value):,}".replace(",", ".") if value_type == "int_no_sep": return str(int(value)) if value_type == "float_dot_sep": # Per Pass. Eff. return f"{float(value):,.1f}".replace(",", ".") if value_type == "date": if isinstance(value, str): for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d'): try: return datetime.strptime(value.split(" ")[0], fmt).strftime('%d/%m/%Y') except ValueError: pass elif hasattr(value, 'strftime'): return value.strftime('%d/%m/%Y') return str(value) return str(value) except (ValueError, TypeError): return str(value) class DetailTableDirittiWidget(QTableWidget): """ Widget tabella di dettaglio specifico per la Dashboard Diritti. """ row_selected = pyqtSignal(str) def __init__(self): super().__init__() self.theme = self._get_default_theme() self.standard_widths = { "Fornitore (cluster)": 200, "Fornitore": 200, "Decorrenza": 90, "Scadenza": 90, "%": 60, "P.Cons.": 90, "P.Eff.": 90, "Caus.": 120, "FR/RR": 60, "Titolo": 200, "Titolo Originale": 200, "Anno": 60, "Paese": 80, "Genere": 150, "N.Episodi": 80, "Durata": 80, "Rif.": 90 } self.sort_column = -1 self.sort_order = Qt.SortOrder.AscendingOrder self.original_data: Optional[pd.DataFrame] = None self.column_mapping = self._get_column_mapping() # Salva mapping per export Excel self._setup_base_table() self.itemClicked.connect(self._on_item_clicked) logger.info("🧩 DetailTableDirittiWidget (specifico per Diritti) inizializzato") def _get_column_config(self) -> Dict: headers = [ "Fornitore (cluster)", "Fornitore", "Decorrenza", "Scadenza", "%", "P.Cons.", "P.Eff.", "Caus.", "FR/RR", "Titolo", "Titolo Originale", "Anno", "Paese", "Genere", "N.Episodi", "Durata", "Rif." ] return {'headers': headers, 'sortable_columns': list(range(len(headers)))} def _get_column_mapping(self) -> Dict[str, str]: return { "Fornitore (cluster)": "fornitore_cluster", "Fornitore": "ragsoc_distr", "Decorrenza": "decr", "Scadenza": "scad", "%": "perc", "P.Cons.": "pass_cons_tot", "P.Eff.": "pass_eff_tot", "Caus.": "causale", "FR/RR": "fr_rr", "Titolo": "titolo_italiano", "Titolo Originale": "titolo_originale", "Anno": "anno_produzione", "Paese": "paesi_produzione1", "Genere": "genere1", "N.Episodi": "num_episodi", "Durata": "durata", "Rif.": "prod" } def populate(self, data: pd.DataFrame): logger.info(f"📊 Popolando DetailTableDirittiWidget con {len(data)} righe.") try: self.clear_data() if data.empty: return self.original_data = data.copy() self.setRowCount(len(data)) self._populate_rows_direct(data) try: header = self.horizontalHeader() headers = self._get_column_config()['headers'] for i, h in enumerate(headers): header.setSectionResizeMode(i, QHeaderView.ResizeMode.Interactive) if h in self.standard_widths: self.setColumnWidth(i, self.standard_widths[h]) except Exception: pass except Exception as e: logger.error(f"❌ Errore popolamento DetailTableDirittiWidget: {e}", exc_info=True) QMessageBox.critical(self, "Errore Dati", f"Impossibile visualizzare i dati di dettaglio.\n\n{e}") def _populate_rows_direct(self, data: pd.DataFrame): column_config = self._get_column_config() column_mapping = self._get_column_mapping() center_aligned_headers = ["Decorrenza", "Scadenza", "FR/RR", "Anno"] right_aligned_headers = ["%", "P.Cons.", "P.Eff.", "Anno", "N.Episodi", "Durata", "Rif."] for row_idx, (_, row_data) in enumerate(data.iterrows()): for col_idx, header_text in enumerate(column_config['headers']): df_col_name = column_mapping.get(header_text) if df_col_name and df_col_name in row_data.index: value = row_data[df_col_name] formatted_value = "" if header_text in ["Decorrenza", "Scadenza"]: formatted_value = safe_format_value(value, "date") elif header_text == "%": formatted_value = safe_format_value(value, "int_no_sep") elif header_text == "P.Cons.": formatted_value = safe_format_value(value, "int_dot_sep") elif header_text == "P.Eff.": formatted_value = safe_format_value(value, "float_dot_sep") elif header_text in ["Anno", "N.Episodi", "Durata", "Rif."]: formatted_value = safe_format_value(value, "int_no_sep") else: formatted_value = safe_format_value(value, "text") item = QTableWidgetItem(formatted_value) if header_text in center_aligned_headers: item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) elif header_text in right_aligned_headers: item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) else: item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) self.setItem(row_idx, col_idx, item) else: self.setItem(row_idx, col_idx, QTableWidgetItem("")) def _get_default_theme(self): return { 'table': { 'header_bg': '#0078D4', 'header_text': '#FFFFFF', 'row_bg_primary': '#FFFFFF', 'row_bg_alternate': '#F5F8FF', 'border': '#EDEBE9', 'selected_bg': '#DEECF9', 'selected_text': '#323130' }, 'fonts': {'system': 'Segoe UI', 'primary_size': 12} } def set_theme(self, theme): self.theme = theme or self._get_default_theme(); self._apply_styling() def clear_data(self): self.setRowCount(0); self.original_data = None def _on_item_clicked(self, item): if not item: return pass def _setup_base_table(self): column_config = self._get_column_config() self.setColumnCount(len(column_config['headers'])) self.setHorizontalHeaderLabels(column_config['headers']) self.horizontalHeader().setSectionsMovable(True) self.setSortingEnabled(False) # Abilita sorting manuale con click su header try: self.horizontalHeader().sectionClicked.disconnect() except TypeError: pass self.horizontalHeader().sectionClicked.connect(self._on_header_clicked) self.verticalHeader().setVisible(False) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.setAlternatingRowColors(True) self._apply_styling() def _apply_styling(self): table_colors = self.theme.get('table', {}) fonts = self.theme.get('fonts', {}) stylesheet = f""" QTableWidget {{ gridline-color: {table_colors.get('border', '#EDEBE9')}; background-color: {table_colors.get('row_bg_primary', '#FFFFFF')}; alternate-background-color: {table_colors.get('row_bg_alternate', '#F5F8FF')}; font-family: "{fonts.get('system', 'Segoe UI')}"; font-size: {fonts.get('primary_size', 12)}px; border: 1px solid {table_colors.get('border', '#EDEBE9')}; }} QHeaderView::section {{ background-color: {table_colors.get('header_bg', '#0078D4')}; color: {table_colors.get('header_text', '#FFFFFF')}; padding: 4px; border: 1px solid {table_colors.get('border', '#EDEBE9')}; font-weight: 600; }} QTableWidget::item {{ padding: 5px; }} QTableWidget::item:selected {{ background-color: {table_colors.get('selected_bg', '#DEECF9')}; color: {table_colors.get('selected_text', '#323130')}; }} """ self.setStyleSheet(stylesheet) def _on_header_clicked(self, logical_index: int): """ Gestisce click su header per ordinamento colonne. Toggle tra ascending/descending per la stessa colonna. """ column_config = self._get_column_config() sortable_columns = column_config.get('sortable_columns', list(range(len(column_config['headers'])))) if logical_index not in sortable_columns: self.horizontalHeader().setSortIndicator(-1, Qt.SortOrder.AscendingOrder) return # Toggle sort order se stessa colonna, altrimenti reset ad ascending if self.sort_column == logical_index: self.sort_order = (Qt.SortOrder.DescendingOrder if self.sort_order == Qt.SortOrder.AscendingOrder else Qt.SortOrder.AscendingOrder) else: self.sort_column = logical_index self.sort_order = Qt.SortOrder.AscendingOrder # Mostra indicatore visivo self.horizontalHeader().setSortIndicator(self.sort_column, self.sort_order) # Esegui ordinamento self._sort_by_column(logical_index) def _sort_by_column(self, column_index: int): """ Ordina il DataFrame per la colonna specificata e ripopola la tabella. """ if self.original_data is None or self.original_data.empty: return headers = self._get_column_config()['headers'] if column_index >= len(headers): return header_text = headers[column_index] column_mapping = self._get_column_mapping() data_column = column_mapping.get(header_text) if not data_column or data_column not in self.original_data.columns: logger.warning(f"Column mapping not found for header '{header_text}'") return ascending = (self.sort_order == Qt.SortOrder.AscendingOrder) try: sorted_data = self.original_data.sort_values(by=data_column, ascending=ascending) self._repopulate_with_sorted_data(sorted_data) except Exception as e: logger.error(f"Error sorting by column {header_text}: {e}", exc_info=True) def _repopulate_with_sorted_data(self, sorted_df: pd.DataFrame): """ Ripopola la tabella con dati ordinati, mantenendo la selezione se possibile. """ # Salva selezione corrente (se esiste) current_selection = None selected_items = self.selectedItems() if selected_items: row = selected_items[0].row() if row < len(self.original_data): # Usa 'prod' come chiave univoca per riga column_mapping = self._get_column_mapping() prod_col = column_mapping.get("Rif.", "prod") if prod_col in self.original_data.columns: current_selection = self.original_data.iloc[row][prod_col] # Ripopola con dati ordinati self.populate(sorted_df) # Ripristina selezione se possibile if current_selection is not None: column_mapping = self._get_column_mapping() prod_col = column_mapping.get("Rif.", "prod") if prod_col in sorted_df.columns: try: new_row = sorted_df[sorted_df[prod_col] == current_selection].index[0] self.selectRow(new_row) except (IndexError, KeyError): pass