""" Finestra principale Linker — multi-foglio. Gestisce i fogli Linker (inner QTabWidget con tab a Sud, stile Excel). Diritti in Scadenza apre in finestra separata (DiritiWindow). La logica griglia è in LinkerSheet (linker_sheet.py). """ import json import logging from pathlib import Path from PyQt6.QtWidgets import ( QMainWindow, QTabWidget, QApplication, QMessageBox, QInputDialog, ) from PyQt6.QtCore import Qt, QEvent from PyQt6.QtGui import QAction from app.modules.linker.src.gui.linker_sheet import ( COLUMNS, COL_RIFER, COL_IMDB, COL_DECORRENZA, ) from app.modules.linker.src.gui.linker_sheet_aggancio import LinkerSheetAggancio as LinkerSheet logger = logging.getLogger(__name__) SESSION_FILE = Path(__file__).parents[2] / 'session.json' MAX_SHEETS = 10 APP_TYPE_LABELS = { 'linker': 'Linker', 'in_scadenza': 'In Scadenza', 'aggancio': 'Aggancio', 'valutazioni_reti': 'Val. Reti', 'prossime_uscite': 'Box. Prossime Uscite', 'ott': 'OTT', 'gemma': 'Gemma', } SHEETS_TAB_STYLE = """ QTabBar::tab { min-width: 100px; padding: 3px 10px; background: #E8E8E8; border: 1px solid #B0B0B0; border-top: none; border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } QTabBar::tab:selected { background: #FFFFFF; font-weight: bold; border-top: 1px solid #FFFFFF; } QTabBar::tab:hover:!selected { background: #F0F0F0; } """ class MainWindow(QMainWindow): def __init__(self, linker_db, catalog=None, dev_mode: bool = False, server_linker_db_path: str = '', ott_parquet_server_path: str = ''): super().__init__() self.linker_db = linker_db self._dev_mode = dev_mode self._server_linker_db_path = server_linker_db_path self._ott_parquet_server_path = ott_parquet_server_path self.setWindowTitle("Linker [DEV]" if dev_mode else "Linker") from app.modules.linker.src.database.diritti_db import DiritiDatabase self.diritti_db = DiritiDatabase(catalog) self._last_diritti_filters = None self._setup_menu() self._setup_sheets_tabs() self._load_session() # -------------------------------------------------------------- # # Proprietà # # -------------------------------------------------------------- # @property def current_sheet(self): """Foglio Linker attualmente attivo (None se la tab corrente non è un LinkerSheet).""" w = self.sheets_tabs.currentWidget() return w if isinstance(w, LinkerSheet) else None # -------------------------------------------------------------- # # Setup UI # # -------------------------------------------------------------- # def _setup_menu(self): menubar = self.menuBar() # ── App launcher — azioni dirette sulla barra ───────────────── act_linker = QAction("Linker", self) act_linker.triggered.connect(self.nuovo_foglio) menubar.addAction(act_linker) act_agg = QAction("Agg. Multiplo", self) act_agg.triggered.connect(self.apri_agg_multiplo) menubar.addAction(act_agg) act_scadenza = QAction("In Scadenza", self) act_scadenza.triggered.connect(self.apri_in_scadenza) menubar.addAction(act_scadenza) act_pu = QAction("Box. Prossime Uscite", self) act_pu.triggered.connect(self.apri_prossime_uscite) menubar.addAction(act_pu) act_ott = QAction("OTT", self) act_ott.triggered.connect(self.apri_ott) menubar.addAction(act_ott) act_gemma = QAction("Gemma", self) act_gemma.triggered.connect(self.apri_gemma) menubar.addAction(act_gemma) pass # Comandi rimosso — tutto sulle toolbar delle singole app def _next_label(self, app_type: str) -> str: """Restituisce la prossima etichetta disponibile per il tipo dato (es. 'Linker 2').""" base = APP_TYPE_LABELS.get(app_type, app_type.capitalize()) existing = set() for i in range(self.sheets_tabs.count()): w = self.sheets_tabs.widget(i) if getattr(w, 'app_type', None) == app_type: existing.add(self.sheets_tabs.tabText(i)) n = 1 while f"{base} {n}" in existing: n += 1 return f"{base} {n}" def _setup_sheets_tabs(self): """QTabWidget fogli Linker — tab a Sud (stile Excel).""" self.sheets_tabs = QTabWidget() self.sheets_tabs.setTabPosition(QTabWidget.TabPosition.South) self.sheets_tabs.setStyleSheet(SHEETS_TAB_STYLE) self.sheets_tabs.setTabsClosable(True) self.sheets_tabs.tabCloseRequested.connect(self._on_close_sheet_btn) tab_bar = self.sheets_tabs.tabBar() tab_bar.installEventFilter(self) tab_bar.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) tab_bar.customContextMenuRequested.connect(self._on_tab_context_menu) self._add_sheet() self.setCentralWidget(self.sheets_tabs) self.statusBar().showMessage("") # -------------------------------------------------------------- # # Gestione fogli # # -------------------------------------------------------------- # def _add_sheet(self, name: str = None) -> LinkerSheet: """Crea un LinkerSheet (app_type='linker') e lo aggiunge all'inner tab widget.""" sheet = LinkerSheet( self.linker_db, status_fn=self.statusBar().showMessage, parent=self, ) sheet.app_type = 'linker' sheet.sig_salva_richiesto.connect(self.salva_agganci_listino) sheet.sig_importa_valutazioni.connect(self.importa_valutazioni) sheet.sig_cancella_valutazioni.connect(self.cancella_valutazioni) sheet.sig_toggle_emesso.connect(self._on_toggle_emesso) sheet.sig_avvia_aggancio.connect(self.aggancia_listino) label = name or self._next_label('linker') idx = self.sheets_tabs.addTab(sheet, label) self.sheets_tabs.setCurrentIndex(idx) return sheet def nuovo_foglio(self): if self.sheets_tabs.count() >= MAX_SHEETS: QMessageBox.warning(self, "Linker", f"Massimo {MAX_SHEETS} fogli raggiunto.") return linker_count = sum( 1 for i in range(self.sheets_tabs.count()) if isinstance(self.sheets_tabs.widget(i), LinkerSheet) ) if linker_count > 0: risposta = QMessageBox.question( self, "Nuovo foglio Linker", f"Hai già {linker_count} {'foglio' if linker_count == 1 else 'fogli'} " f"Linker {'aperto' if linker_count == 1 else 'aperti'}. Aprire un altro?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if risposta != QMessageBox.StandardButton.Yes: return self._add_sheet() def rinomina_foglio(self): idx = self.sheets_tabs.currentIndex() if idx < 0: return current_name = self.sheets_tabs.tabText(idx) name, ok = QInputDialog.getText( self, "Rinomina foglio", "Nuovo nome:", text=current_name ) if ok and name.strip(): self.sheets_tabs.setTabText(idx, name.strip()) def elimina_foglio(self): self._close_sheet(self.sheets_tabs.currentIndex()) def _on_close_sheet_btn(self, idx: int): self._close_sheet(idx) def _close_sheet(self, idx: int): widget = self.sheets_tabs.widget(idx) # La tab Diritti si chiude sempre senza conferma if not isinstance(widget, LinkerSheet): self.sheets_tabs.removeTab(idx) return # Un LinkerSheet non si elimina se è l'unico rimasto linker_count = sum( 1 for i in range(self.sheets_tabs.count()) if isinstance(self.sheets_tabs.widget(i), LinkerSheet) ) if linker_count <= 1: return if widget.has_data(): name = self.sheets_tabs.tabText(idx) risposta = QMessageBox.question( self, "Elimina foglio", f"Il foglio '{name}' contiene dati. Eliminare?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if risposta != QMessageBox.StandardButton.Yes: return self.sheets_tabs.removeTab(idx) def eventFilter(self, obj, event): """Doppio click sul tab bar → rinomina (solo LinkerSheet).""" if obj is self.sheets_tabs.tabBar() and event.type() == QEvent.Type.MouseButtonDblClick: idx = self.sheets_tabs.tabBar().tabAt(event.pos()) if idx >= 0 and isinstance(self.sheets_tabs.widget(idx), LinkerSheet): self.rinomina_foglio() return True return super().eventFilter(obj, event) def _on_tab_context_menu(self, pos): """Context menu tasto destro sulla tab bar.""" from PyQt6.QtWidgets import QMenu tab_bar = self.sheets_tabs.tabBar() idx = tab_bar.tabAt(pos) if idx < 0: return is_linker = isinstance(self.sheets_tabs.widget(idx), LinkerSheet) if not is_linker: return # nessun menu per app non rinominabili (In Scadenza, ecc.) menu = QMenu(self) act_rename = menu.addAction("Rinomina foglio") act_rename.triggered.connect(self.rinomina_foglio) menu.exec(tab_bar.mapToGlobal(pos)) # -------------------------------------------------------------- # # Azioni menu (delegate a current_sheet) # # -------------------------------------------------------------- # def apri_agg_multiplo(self): """Apre un foglio Aggancio Multiplo; se già aperto chiede se aprirne un altro.""" existing = [i for i in range(self.sheets_tabs.count()) if getattr(self.sheets_tabs.widget(i), 'app_type', None) == 'aggancio'] if existing: count = len(existing) risposta = QMessageBox.question( self, "Aggancio Multiplo", f"Hai già {count} {'foglio' if count == 1 else 'fogli'} " f"Aggancio Multiplo {'aperto' if count == 1 else 'aperti'}. Aprire un altro?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if risposta != QMessageBox.StandardButton.Yes: self.sheets_tabs.setCurrentIndex(existing[-1]) return # Chiedi se usare il wizard (gate per aggancio; non influisce su valutazioni) use_wizard = self._chiedi_wizard() # Chiedi sempre cosa fare from app.modules.linker.src.gui.wizard_selector import FunzionalitaDialog dlg = FunzionalitaDialog(self) if dlg.exec() != dlg.DialogCode.Accepted or not dlg.get_selected(): return selected = dlg.get_selected() if selected == 'valutazioni_reti': self._apri_valutazioni_reti(use_wizard) return # Aggancio titoli label = self._next_label('aggancio') sheet = self._add_sheet(name=label) sheet.app_type = 'aggancio' sheet._set_mode("AGG. MULTIPLO", "#DDD0F8") sheet.show_feeder_only() if use_wizard: sheet.start_wizard() def _chiedi_wizard(self) -> bool: """Controlla le preferenze e chiede se usare il wizard. Restituisce True/False.""" from app.modules.linker import prefs from app.modules.linker.src.gui.wizard_selector import WizardOptInDialog pref = prefs.get('wizard_aggancio') if pref is True: return True if pref is False: return False # pref è None → chiedi dlg = WizardOptInDialog(self) dlg.exec() use = dlg.use_wizard() if dlg.remember(): prefs.set_pref('wizard_aggancio', use) return use def _apri_valutazioni_reti(self, use_wizard: bool = False): """Crea un nuovo foglio Valutazioni Reti e lo aggiunge come tab.""" from app.modules.linker.src.gui.valutazioni_reti_sheet import ValutazioniRetiSheet sheet = ValutazioniRetiSheet(linker_db=self.linker_db, parent=self) label = self._next_label('valutazioni_reti') idx = self.sheets_tabs.addTab(sheet, label) self.sheets_tabs.setCurrentIndex(idx) if use_wizard: sheet.start_wizard() def aggancia_listino(self): """Aggancio sul foglio corrente (da menu Comandi — mantiene compatibilità).""" if self.current_sheet is None: return from app.modules.linker.src.gui.aggancia_dialog import AgganciaDialog from app.modules.linker.src.gui.linker_sheet import COL_TO, COL_TI sheet = self.current_sheet titles = [] for row in range(min(sheet.table.rowCount(), 10)): for col in (COL_TO, COL_TI): item = sheet.table.item(row, col) if item: t = item.text().strip() if t: titles.append(t) break dlg = AgganciaDialog(linker_db=self.linker_db, titles=titles, parent=self) if dlg.exec() != dlg.DialogCode.Accepted: return is_listino, fornitore, is_seriale = dlg.get_result() sheet.aggancia(is_listino, fornitore, is_seriale) def salva_agganci_listino(self): sheet = self.current_sheet if sheet is None: return from app.modules.linker.src.gui.aggancia_dialog import SalvaFornitoreDialog dlg = SalvaFornitoreDialog( linker_db=self.linker_db, current_fornitore=sheet.current_fornitore, parent=self, ) if dlg.exec() != dlg.DialogCode.Accepted: return fornitore = dlg.get_fornitore() records = sheet.get_saveable_records() if not records: QMessageBox.information(self, "Salvataggio", "Nessun aggancio da salvare.") return try: QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) nuovi, aggiornati = self.linker_db.save_to_repository(records, fornitore) sheet.mark_saved_rows_as_repository() QApplication.restoreOverrideCursor() parti = [] if nuovi: parti.append(f"{nuovi} nuovi") if aggiornati: parti.append(f"{aggiornati} aggiornati") if not parti: msg = "Nessun record nuovo (tutti già presenti in LINKER_REPOSITORY)." else: msg = f"{' · '.join(parti)} agganci salvati in LINKER_REPOSITORY." QMessageBox.information(self, "Salvataggio", msg) except Exception as e: QApplication.restoreOverrideCursor() QMessageBox.critical(self, "Errore salvataggio", str(e)) logger.error(f"Errore salva_agganci_listino: {e}", exc_info=True) def annulla_aggancio(self): if self.current_sheet is None: return self.current_sheet.annulla() def cancella_tutto(self): if self.current_sheet is None: return risposta = QMessageBox.question( self, "Cancella tutto", "Cancellare tutti i record nel foglio?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if risposta != QMessageBox.StandardButton.Yes: return self.current_sheet.cancella() def get_active_linker_context(self) -> dict: """ Raccoglie IMDB codes, RIFER e RIFER→DECORRENZA da tutti i fogli Linker attivi. Usato da InScadenza per i filtri di esclusione — il contenitore è l'unico che conosce i fogli aperti; InScadenza non li interroga direttamente. """ imdb_codes = set() rifer_codes = set() rifer_decorrenza = {} for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if not isinstance(sheet, LinkerSheet): continue is_aggancio = getattr(sheet, 'app_type', None) == 'aggancio' for row in range(sheet.table.rowCount()): imdb_item = sheet.table.item(row, COL_IMDB) rifer_item = sheet.table.item(row, COL_RIFER) decor_item = sheet.table.item(row, COL_DECORRENZA) if imdb_item: val = imdb_item.text().strip() if val: imdb_codes.add(val) if rifer_item: rifer = rifer_item.text().strip() if rifer: rifer_codes.add(rifer) # DECOR. LISTINO è significativa solo dai fogli Aggancio if is_aggancio and decor_item: decor = decor_item.text().strip() if decor: rifer_decorrenza[rifer] = decor return { 'imdb_codes': imdb_codes, 'rifer_codes': rifer_codes, 'rifer_decorrenza': rifer_decorrenza, } def apri_in_scadenza(self): """Apre la tab In Scadenza; se già aperta chiede se aprirne un'altra.""" from app.modules.linker.src.gui.diritti_tab import DiritiTab existing = [i for i in range(self.sheets_tabs.count()) if isinstance(self.sheets_tabs.widget(i), DiritiTab)] if existing: count = len(existing) risposta = QMessageBox.question( self, "In Scadenza", f"Hai già {count} {'foglio' if count == 1 else 'fogli'} " f"In Scadenza {'aperto' if count == 1 else 'aperti'}. Aprire un altro?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if risposta != QMessageBox.StandardButton.Yes: self.sheets_tabs.setCurrentIndex(existing[-1]) return # Crea tab, poi mostra parametri from app.modules.linker.src.gui.diritti_tab import DiritiFilterDialog try: options = self.diritti_db.get_filter_options() except Exception as e: logger.error(f"Errore get_filter_options: {e}", exc_info=True) QMessageBox.critical(self, "Errore", f"Impossibile caricare i filtri:\n{e}") return tab = DiritiTab(self.diritti_db, self) tab.app_type = 'in_scadenza' tab.sig_filtro_richiesto.connect(self._parametri_in_scadenza) label = self._next_label('in_scadenza') diritti_idx = self.sheets_tabs.addTab(tab, label) self.sheets_tabs.setCurrentIndex(diritti_idx) ctx = self.get_active_linker_context() dlg = DiritiFilterDialog( options, imdb_codes=ctx['imdb_codes'], rifer_codes=ctx['rifer_codes'], rifer_decorrenza=ctx['rifer_decorrenza'], previous_filters=self._last_diritti_filters, parent=self, ) if dlg.exec() != DiritiFilterDialog.DialogCode.Accepted: self.sheets_tabs.removeTab(diritti_idx) return filters = dlg.get_filters() self._last_diritti_filters = filters tab.load_with_filters(filters) def apri_prossime_uscite(self): """Apre un tab Boxoffice - Prossime Uscite.""" from app.modules.linker.src.gui.prossime_uscite_sheet import ProssimeUsciteSheet sheet = ProssimeUsciteSheet(linker_db=self.linker_db, parent=self) label = self._next_label('prossime_uscite') idx = self.sheets_tabs.addTab(sheet, label) self.sheets_tabs.setCurrentIndex(idx) def apri_ott(self): """Apre un tab OTT (finestre di pubblicazione streaming).""" from app.modules.linker.src.gui.ott_sheet import OttSheet sheet = OttSheet( linker_db=self.linker_db, dev_mode=self._dev_mode, ott_parquet_server_path=self._ott_parquet_server_path, linker_server_path=self._server_linker_db_path, parent=self, ) label = self._next_label('ott') idx = self.sheets_tabs.addTab(sheet, label) self.sheets_tabs.setCurrentIndex(idx) def apri_gemma(self): """Apre un tab Gemma (read-only, tutti i record di gemma.parquet).""" from app.modules.linker.src.gui.gemma_sheet import GemmaSheet from app.config_loader import get_app_root, load_config root = get_app_root() cfg = load_config() parquet_path = str(root / cfg.get('parquet_dir', 'local_db/parquet')) sheet = GemmaSheet(parquet_path=parquet_path, parent=self) label = self._next_label('gemma') idx = self.sheets_tabs.addTab(sheet, label) self.sheets_tabs.setCurrentIndex(idx) def _parametri_in_scadenza(self): """Riapre il dialog parametri sulla tab In Scadenza attualmente attiva.""" from app.modules.linker.src.gui.diritti_tab import DiritiFilterDialog, DiritiTab current = self.sheets_tabs.currentWidget() if not isinstance(current, DiritiTab): return tab = current try: options = self.diritti_db.get_filter_options() except Exception as e: QMessageBox.critical(self, "Errore", f"Impossibile caricare i filtri:\n{e}") return ctx = self.get_active_linker_context() dlg = DiritiFilterDialog( options, imdb_codes=ctx['imdb_codes'], rifer_codes=ctx['rifer_codes'], rifer_decorrenza=ctx['rifer_decorrenza'], previous_filters=self._last_diritti_filters, parent=self, ) if dlg.exec() != DiritiFilterDialog.DialogCode.Accepted: return filters = dlg.get_filters() self._last_diritti_filters = filters tab.load_with_filters(filters) # -------------------------------------------------------------- # # Comandi generali — Valutazioni # # -------------------------------------------------------------- # def importa_valutazioni(self): imdb_codes = set() for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if not isinstance(sheet, LinkerSheet): continue for row in range(sheet.table.rowCount()): item = sheet.table.item(row, COL_IMDB) if item: val = item.text().strip() if val and val.lower().startswith('tt'): imdb_codes.add(val) if not imdb_codes: QMessageBox.information(self, "Valutazioni", "Nessun codice IMDB trovato nei fogli attivi.") return try: QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) df = self.linker_db.get_gemma_valutazioni_by_imdb_codes(list(imdb_codes)) except Exception as e: logger.error(f"importa_valutazioni: {e}", exc_info=True) QMessageBox.critical(self, "Errore", f"Errore caricamento Gemma:\n{e}") return finally: QApplication.restoreOverrideCursor() gemma_lookup = {} if not df.empty: for _, row in df.iterrows(): key = str(row.get('A_COD_IMDB', '') or '').strip() if key: gemma_lookup[key] = row.to_dict() extra_gemma_map = self.linker_db.get_valutazioni_extra_gemma_full_map() for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if isinstance(sheet, LinkerSheet): sheet.show_valutazioni(gemma_lookup, extra_gemma_map) found = len(gemma_lookup) self.statusBar().showMessage( f"Valutazioni importate: {found} codici trovati su {len(imdb_codes)}." ) def cancella_valutazioni(self): for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if isinstance(sheet, LinkerSheet): sheet.hide_valutazioni() self.statusBar().showMessage("Valutazioni nascoste.") def _on_toggle_emesso(self, checked: bool): self._emesso_show_first = checked for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if isinstance(sheet, LinkerSheet): sheet._emesso_show_first = checked sheet.set_emesso_mode(checked) sheet.refresh_emesso() mode = "prima" if checked else "ultima" self.statusBar().showMessage(f"Emissione: {mode}.") def toggle_emesso_mode(self): self._on_toggle_emesso(not getattr(self, '_emesso_show_first', False)) # -------------------------------------------------------------- # # Export Excel # # -------------------------------------------------------------- # def esporta(self): import openpyxl from openpyxl.styles import Font, PatternFill, Alignment from openpyxl.utils import get_column_letter from PyQt6.QtWidgets import QFileDialog from app.modules.linker.src.gui.diritti_tab import DiritiTab current_widget = self.sheets_tabs.currentWidget() wb = openpyxl.Workbook() if isinstance(current_widget, DiritiTab): from app.modules.linker.src.gui.diritti_tab import ALL_COLUMNS as DIRITTI_COLS, FIRST_DIRITTI_COL rows_data = current_widget.get_export_data() if not rows_data: QMessageBox.information(self, "Esporta", "Nessun dato da esportare.") return path, _ = QFileDialog.getSaveFileName( self, "Esporta Diritti in Scadenza", "diritti_scadenza.xlsx", "Excel (*.xlsx)" ) if not path: return ws = wb.active ws.title = "Diritti in Scadenza" header_font = Font(bold=True) for col_idx, label in enumerate(DIRITTI_COLS, start=1): cell = ws.cell(row=1, column=col_idx, value=label) cell.font = header_font cell.alignment = Alignment(horizontal='center') if col_idx > FIRST_DIRITTI_COL: cell.fill = PatternFill('solid', fgColor='BDD7EE') for out_row, row_vals in enumerate(rows_data, start=2): for col_idx, val in enumerate(row_vals, start=1): ws.cell(row=out_row, column=col_idx, value=val) for col_idx, label in enumerate(DIRITTI_COLS, start=1): ws.column_dimensions[get_column_letter(col_idx)].width = max(10, len(label) + 4) else: sheet = self.current_sheet if sheet is None: return sheet_name = self.sheets_tabs.tabText(self.sheets_tabs.currentIndex()) rows_data, match_sources = sheet.get_export_rows() if not rows_data: QMessageBox.information(self, "Esporta", "Nessun dato da esportare.") return path, _ = QFileDialog.getSaveFileName( self, "Esporta griglia", "linker_export.xlsx", "Excel (*.xlsx)" ) if not path: return MATCH_FILL = { 'REPOSITORY': 'ADD8E6', 'ONAIR': '90EE90', 'IMDB': 'FFFF99', 'DERIVED': 'FFFF99', 'NOT_FOUND': 'FF9999', } ws = wb.active ws.title = sheet_name[:31] header_font = Font(bold=True) for col_idx, label in enumerate(COLUMNS, start=1): cell = ws.cell(row=1, column=col_idx, value=label) cell.font = header_font cell.alignment = Alignment(horizontal='center') for i, row_vals in enumerate(rows_data): out_row = i + 2 fill_hex = MATCH_FILL.get(match_sources[i]) fill = PatternFill('solid', fgColor=fill_hex) if fill_hex else None for col_idx, val in enumerate(row_vals, start=1): cell = ws.cell(row=out_row, column=col_idx, value=val) if fill: cell.fill = fill for col_idx, label in enumerate(COLUMNS, start=1): ws.column_dimensions[get_column_letter(col_idx)].width = max(10, len(label) + 4) try: wb.save(path) self.statusBar().showMessage(f"Esportato: {path}") except Exception as e: QMessageBox.critical(self, "Errore esportazione", str(e)) logger.error(f"Errore esporta: {e}", exc_info=True) # -------------------------------------------------------------- # # Sessione (formato v2 multi-foglio) # # -------------------------------------------------------------- # def closeEvent(self, event): self._save_session() self._save_aggancio_restore() if self.linker_db.server_con is not None: try: self.linker_db.server_con.close() except Exception: pass event.accept() def _save_session(self): """Salva solo la struttura (tipo tab, nome, stato wizard) — niente righe.""" from app.modules.linker.src.gui.valutazioni_reti_sheet import ValutazioniRetiSheet from app.modules.linker.src.gui.prossime_uscite_sheet import ProssimeUsciteSheet from app.modules.linker.src.gui.ott_sheet import OttSheet _saveable_types = (LinkerSheet, ValutazioniRetiSheet, ProssimeUsciteSheet, OttSheet) sheets_data = [] for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if not isinstance(sheet, _saveable_types): continue app_type = getattr(sheet, 'app_type', 'linker') entry = { 'app_type': app_type, 'name': self.sheets_tabs.tabText(i), 'rows': [], } if getattr(sheet, '_wizard_mode', False): entry['wizard_active'] = True entry['wizard_step'] = getattr(sheet, '_wizard_bar', None) and sheet._wizard_bar.current_step() or 0 sheets_data.append(entry) try: with open(SESSION_FILE, 'w', encoding='utf-8') as f: json.dump({'version': 3, 'sheets': sheets_data}, f, ensure_ascii=False, indent=2) logger.info(f"Sessione salvata: {len(sheets_data)} fogli → {SESSION_FILE}") except Exception as e: logger.warning(f"Impossibile salvare sessione: {e}") def _load_session(self): if not SESSION_FILE.exists(): return try: with open(SESSION_FILE, 'r', encoding='utf-8') as f: data = json.load(f) except Exception as e: logger.warning(f"Impossibile caricare sessione: {e}") return version = data.get('version', 1) if version < 2: logger.info("session.json formato v1 — ignorato (ripartenza da zero)") return sheets = data.get('sheets', []) if not sheets: return # Rimuovi il foglio di default creato da _setup_sheets_tabs while self.sheets_tabs.count() > 0: self.sheets_tabs.removeTab(0) from app.modules.linker.src.gui.valutazioni_reti_sheet import ValutazioniRetiSheet import re for sheet_data in sheets: # v2 non ha app_type → retrocompatibile come 'linker' app_type = sheet_data.get('app_type', 'linker') name = sheet_data.get('name') or '' # Migra nomi default vecchio formato "Foglio N" → nuovo naming if re.fullmatch(r'Foglio\s+\d+', name): name = None # lascia generare a _next_label if app_type == 'valutazioni_reti': sheet = ValutazioniRetiSheet(linker_db=self.linker_db, parent=self) label = name or self._next_label('valutazioni_reti') self.sheets_tabs.addTab(sheet, label) elif app_type == 'prossime_uscite': from app.modules.linker.src.gui.prossime_uscite_sheet import ProssimeUsciteSheet sheet = ProssimeUsciteSheet(linker_db=self.linker_db, parent=self) label = name or self._next_label('prossime_uscite') self.sheets_tabs.addTab(sheet, label) elif app_type == 'ott': from app.modules.linker.src.gui.ott_sheet import OttSheet sheet = OttSheet( linker_db=self.linker_db, dev_mode=self._dev_mode, ott_parquet_server_path=self._ott_parquet_server_path, linker_server_path=self._server_linker_db_path, parent=self, ) label = name or self._next_label('ott') self.sheets_tabs.addTab(sheet, label) else: sheet = self._add_sheet(name) sheet.app_type = app_type if app_type == 'aggancio': sheet._set_mode("AGG. MULTIPLO", "#DDD0F8") sheet.show_feeder_only() # wizard_active/wizard_step: ripristinati in _offer_aggancio_restore # rows: non vengono più ripristinati qui — passano dal dialog opt-in # Safety: almeno un foglio sempre presente if self.sheets_tabs.count() == 0: self._add_sheet() logger.info(f"Sessione caricata: {len(sheets)} fogli (v{version}) da {SESSION_FILE}") # Differisce il dialog di restore a dopo che la finestra è visibile from PyQt6.QtCore import QTimer QTimer.singleShot(200, self._offer_aggancio_restore) # ---------------------------------------------------------------- # # Restore sessione (tutti i fogli, opt-in) # # ---------------------------------------------------------------- # _RESTORE_FILE = SESSION_FILE.parent / 'session_restore.json' def _save_aggancio_restore(self): """Salva i dati completi di tutti i fogli con dati in session_restore.json.""" from app.modules.linker.src.gui.valutazioni_reti_sheet import ValutazioniRetiSheet restore = {} for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if not isinstance(sheet, (LinkerSheet, ValutazioniRetiSheet)) or not sheet.has_data(): continue name = self.sheets_tabs.tabText(i) rows = sheet.get_rows_data() restore[name] = { 'rows': rows, 'row_count': len(rows), 'wizard_active': getattr(sheet, '_wizard_mode', False), 'wizard_step': sheet._wizard_bar.current_step() if getattr(sheet, '_wizard_mode', False) else 0, } if restore: try: self._RESTORE_FILE.write_text( json.dumps(restore, ensure_ascii=False, indent=2), encoding='utf-8', ) total = sum(v['row_count'] for v in restore.values()) logger.info(f"Session restore salvato: {len(restore)} fogli, {total} righe") except Exception as e: logger.warning(f"Impossibile salvare session restore: {e}") else: self._RESTORE_FILE.unlink(missing_ok=True) def _offer_aggancio_restore(self): """Offre il ripristino della sessione precedente con un unico dialog.""" if not self._RESTORE_FILE.exists(): return try: restore = json.loads(self._RESTORE_FILE.read_text(encoding='utf-8')) except Exception: self._RESTORE_FILE.unlink(missing_ok=True) return # Conta fogli e titoli disponibili fogli_con_dati = { name: entry for name, entry in restore.items() if entry.get('rows') } if not fogli_con_dati: self._RESTORE_FILE.unlink(missing_ok=True) return n_fogli = len(fogli_con_dati) n_titoli = sum(v['row_count'] for v in fogli_con_dati.values()) nomi = ', '.join(f'«{n}»' for n in fogli_con_dati) risposta = QMessageBox.question( self, "Ripristina sessione precedente", f"È disponibile una sessione precedente con " f"{n_fogli} {'foglio' if n_fogli == 1 else 'fogli'} " f"({n_titoli} titoli totali):\n{nomi}\n\n" f"Vuoi ripristinare?\n" f"(Potrebbero volerci alcuni secondi)", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes, ) if risposta == QMessageBox.StandardButton.Yes: from app.modules.linker.src.gui.valutazioni_reti_sheet import ValutazioniRetiSheet for i in range(self.sheets_tabs.count()): sheet = self.sheets_tabs.widget(i) if not isinstance(sheet, (LinkerSheet, ValutazioniRetiSheet)): continue name = self.sheets_tabs.tabText(i) entry = fogli_con_dati.get(name) if not entry: continue sheet.restore_rows_data(entry['rows']) if entry.get('wizard_active'): step = entry.get('wizard_step', 0) sheet.start_wizard() if step > 0: sheet.wizard_advance(step) self._RESTORE_FILE.unlink(missing_ok=True)