""" ValutazioniRetiSheet — foglio per inserimento valutazioni restituite dalle reti. Colonne: C5, I1, R4, LA5, I2, IRIS, TOP CRIME, FOCUS, C20, CINE34, TWENTY SEVEN, IMDB Wizard: Incolla → Verifica IMDB → Archivia """ import time as _time from datetime import date from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QColor, QFont, QKeySequence, QPainter, QAction from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QFrame, QTableWidget, QTableWidgetItem, QHeaderView, QApplication, QLabel, QPushButton, QToolButton, QMenu, QDialog, QComboBox, QLineEdit, QMessageBox, QSizePolicy, QProgressBar, ) from app.modules.linker.src.gui.grid_filler import ( paint_std_header_section, HEADER_HEIGHT, HEADER_BORDER_COLOR, HEADER_SEL_COLOR, SelectableVHeader, ) from app.modules.linker.src.gui.wizard_bar import WizardBarValutazioni from app.modules.linker.src.gui.wizard_hint_panel import WizardHintPanel, _HINTS_VALUTAZIONI # ── Colonne ──────────────────────────────────────────────────────────────── _COLUMNS = [ 'C5', 'I1', 'R4', 'LA5', 'I2', 'IRIS', 'TOP CRIME', 'FOCUS', 'C20', 'CINE34', 'TWENTY SEVEN', 'IMDB', ] _COL_IMDB = len(_COLUMNS) - 1 _COL_WIDTHS = { 'C5': 45, 'I1': 45, 'R4': 45, 'LA5': 45, 'I2': 45, 'IRIS': 45, 'TOP CRIME': 70, 'FOCUS': 55, 'C20': 45, 'CINE34': 55, 'TWENTY SEVEN': 80, 'IMDB': 90, } _BG_YELLOW = QColor('#FFFF00') _BG_CYAN = QColor('#00BFFF') _BG_NO_IMDB = QColor('#FFCCCC') _NO_IMDB_TOKENS = {'no imdb', 'noimdb', 'n/a', '-'} def _imdb_mancante(val: str) -> bool: """True se il valore IMDB è assente o equivalente a 'nessun codice'.""" v = val.strip().lower() return not v or v in _NO_IMDB_TOKENS _TOOLBAR_COLOR = '#DDD0F8' _BTN_STYLE = ( "QPushButton { background-color: #9B7FD4; color: white;" " border: 1px solid #7A5FB0; border-radius: 3px; padding: 0 10px; }" "QPushButton:hover { background-color: #8A6EC3; }" ) _INITIAL_ROWS = 50 _CELL_FONT = QFont('Calibri', 8) # ── Header colorato ──────────────────────────────────────────────────────── class _ValHeader(QHeaderView): def __init__(self, parent=None): super().__init__(Qt.Orientation.Horizontal, parent) self._table = parent self._sel_cols: set[int] = set() self.setSectionsClickable(True) self.setSortIndicatorShown(True) self.setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.setFixedHeight(HEADER_HEIGHT) def mousePressEvent(self, event): col = self.logicalIndexAt(event.position().toPoint()) if col >= 0 and event.button() == Qt.MouseButton.LeftButton: if event.modifiers() & Qt.KeyboardModifier.ControlModifier: if col in self._sel_cols: self._sel_cols.discard(col) else: self._sel_cols.add(col) else: self._sel_cols = {col} self.viewport().update() super().mousePressEvent(event) def get_selected_cols(self) -> list[int]: return sorted(self._sel_cols) def paintSection(self, painter: QPainter, rect, logicalIndex: int): painter.save() if logicalIndex in self._sel_cols: bg = HEADER_SEL_COLOR elif logicalIndex == _COL_IMDB: bg = _BG_CYAN else: bg = _BG_YELLOW text = self.model().headerData( logicalIndex, Qt.Orientation.Horizontal, Qt.ItemDataRole.DisplayRole ) paint_std_header_section(painter, rect, str(text) if text else '', bg, HEADER_BORDER_COLOR) if self.isSortIndicatorShown() and self.sortIndicatorSection() == logicalIndex: arrow = '▲' if self.sortIndicatorOrder() == Qt.SortOrder.AscendingOrder else '▼' painter.drawText( rect.adjusted(0, 0, -2, 0), Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop, arrow, ) painter.restore() # ── Tabella con paste ────────────────────────────────────────────────────── class _ValTable(QTableWidget): def keyPressEvent(self, event): ctrl = Qt.KeyboardModifier.ControlModifier key = event.key() mods = event.modifiers() if key == Qt.Key.Key_V and mods & ctrl: self._handle_paste() return if key == Qt.Key.Key_C and mods & ctrl: self._handle_copy() return if key in (Qt.Key.Key_Delete, Qt.Key.Key_Backspace): self._handle_delete() return super().keyPressEvent(event) def contextMenuEvent(self, event): menu = QMenu(self) has_sel = bool(self.selectedIndexes()) act_copy = QAction('Copia', self) act_copy.setShortcut(QKeySequence.StandardKey.Copy) act_copy.setEnabled(has_sel) act_copy.triggered.connect(self._handle_copy) menu.addAction(act_copy) act_paste = QAction('Incolla', self) act_paste.setShortcut(QKeySequence.StandardKey.Paste) act_paste.setEnabled(bool(QApplication.clipboard().text())) act_paste.triggered.connect(self._handle_paste) menu.addAction(act_paste) menu.addSeparator() act_del = QAction('Cancella celle', self) act_del.setEnabled(has_sel) act_del.triggered.connect(self._handle_delete) menu.addAction(act_del) menu.exec(event.globalPos()) def _handle_copy(self): indexes = self.selectedIndexes() if not indexes: return rows = sorted({i.row() for i in indexes}) cols = sorted({i.column() for i in indexes}) lines = [] for r in rows: cells = [(self.item(r, c).text() if self.item(r, c) else '') for c in cols] lines.append('\t'.join(cells)) QApplication.clipboard().setText('\n'.join(lines)) def _handle_delete(self): for index in self.selectedIndexes(): item = self.item(index.row(), index.column()) if item: item.setText('') def _handle_paste(self): import csv, io text = QApplication.clipboard().text() if not text: return # csv.reader gestisce campi quotati con newline embedded (quoting TSV Excel) rows_data = [row for row in csv.reader(io.StringIO(text), delimiter='\t') if any(v.strip() for v in row)] if not rows_data: return # Ancora top-left della selezione corrente ranges = self.selectedRanges() if ranges: start_row = ranges[0].topRow() start_col = ranges[0].leftColumn() else: start_row = max(self.currentRow(), 0) start_col = max(self.currentColumn(), 0) needed_rows = start_row + len(rows_data) self.setSortingEnabled(False) try: if needed_rows > self.rowCount(): self.setRowCount(needed_rows) for r_off, cells in enumerate(rows_data): for c_off, val in enumerate(cells): c = start_col + c_off if c >= self.columnCount(): break it = QTableWidgetItem(val.strip()) it.setFont(_CELL_FONT) self.setItem(start_row + r_off, c, it) # Taglia tutto oltre l'area incollata self.setRowCount(start_row + len(rows_data)) finally: self.horizontalHeader().setSortIndicator(-1, Qt.SortOrder.AscendingOrder) self.setSortingEnabled(True) def _fmt_seconds(s: float) -> str: s = int(s) if s < 60: return f'{s}s' return f'{s // 60}m {s % 60:02d}s' # ── Worker salvataggio ───────────────────────────────────────────────────── class _SalvaWorker(QThread): chunk_done = pyqtSignal(int, int) # saved_so_far, total finished = pyqtSignal(int, int) # salvate, saltate error = pyqtSignal(str) def __init__(self, db, rows, fornitore, data, parent=None): super().__init__(parent) self._db = db self._rows = rows self._fornitore = fornitore self._data = data def run(self): try: salvate, saltate = self._db.salva_valutazioni( self._rows, self._fornitore, self._data, on_chunk=lambda saved, total: self.chunk_done.emit(saved, total), ) self.finished.emit(salvate, saltate) except Exception as e: self.error.emit(str(e)) # ── Dialog progress salvataggio ──────────────────────────────────────────── class _SalvaProgressDialog(QDialog): def __init__(self, total: int, parent=None): super().__init__(parent) from app.modules.linker.src.gui.dialog_styles import ( DIALOG_BG, BTN_PRIMARY, TEXT_MAIN, TEXT_SUB, DANGER, ) self._DANGER = DANGER self._TEXT_MAIN = TEXT_MAIN self._TEXT_SUB = TEXT_SUB self._total = total self._done = False self._start = None self.setWindowTitle('Archiviazione valutazioni') self.setFixedWidth(420) self.setWindowFlags( self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint & ~Qt.WindowType.WindowCloseButtonHint ) self.setStyleSheet(DIALOG_BG) layout = QVBoxLayout(self) layout.setContentsMargins(24, 24, 24, 20) layout.setSpacing(8) title = QLabel('Archiviazione valutazioni') title.setStyleSheet(f'color: {TEXT_MAIN}; font-size: 14px; font-weight: 700;') layout.addWidget(title) layout.addSpacing(10) self._bar = QProgressBar() self._bar.setRange(0, max(total, 1)) self._bar.setValue(0) self._bar.setTextVisible(False) self._bar.setFixedHeight(10) self._bar.setStyleSheet( 'QProgressBar { background:#E0E0E0; border-radius:5px; border:none; }' 'QProgressBar::chunk { background:#9B7FD4; border-radius:5px; }' ) layout.addWidget(self._bar) layout.addSpacing(6) self._lbl_count = QLabel(f'0 / {total} record salvati') self._lbl_count.setStyleSheet(f'color:{TEXT_MAIN}; font-size:12px;') layout.addWidget(self._lbl_count) self._lbl_time = QLabel('Calcolo tempo rimanente...') self._lbl_time.setStyleSheet(f'color:{TEXT_SUB}; font-size:11px;') layout.addWidget(self._lbl_time) layout.addSpacing(16) btn_row = QHBoxLayout() btn_row.addStretch() self._btn_ok = QPushButton('OK') self._btn_ok.setStyleSheet(BTN_PRIMARY) self._btn_ok.setFixedHeight(26) self._btn_ok.setEnabled(False) self._btn_ok.clicked.connect(self.accept) btn_row.addWidget(self._btn_ok) layout.addLayout(btn_row) def closeEvent(self, event): if not self._done: event.ignore() else: super().closeEvent(event) def keyPressEvent(self, event): if not self._done and event.key() in (Qt.Key.Key_Escape, Qt.Key.Key_Return): return super().keyPressEvent(event) def on_chunk(self, saved: int, total: int): if self._start is None: self._start = _time.monotonic() self._bar.setValue(saved) pct = int(saved * 100 / total) if total else 0 self._lbl_count.setText(f'{saved} / {total} record salvati • {pct}%') elapsed = _time.monotonic() - self._start if saved > 0 and elapsed > 0: remaining = (total - saved) / (saved / elapsed) self._lbl_time.setText(f'Tempo stimato rimanente: {_fmt_seconds(remaining)}') def on_finished(self, salvate: int, saltate: int): self._done = True self._bar.setValue(self._total) parts = [f'{salvate} record archiviati'] if saltate: parts.append(f'{saltate} saltati (senza IMDB)') self._lbl_count.setText(' · '.join(parts)) elapsed = _time.monotonic() - self._start if self._start else 0 self._lbl_time.setText(f'Completato in {_fmt_seconds(elapsed)}') self._btn_ok.setEnabled(True) self._btn_ok.setFocus() def on_error(self, msg: str): self._done = True self._lbl_count.setStyleSheet(f'color:{self._DANGER}; font-size:12px;') self._lbl_count.setText(f'Errore: {msg}') self._lbl_time.setText('') self._btn_ok.setEnabled(True) self._btn_ok.setFocus() # ── Dialog di salvataggio ────────────────────────────────────────────────── class _SalvaDialog(QDialog): def __init__(self, fornitori: list[str], n_righe: int, n_no_imdb: int, parent=None): super().__init__(parent) from app.modules.linker.src.gui.dialog_styles import ( DIALOG_BG, INPUT, BTN_PRIMARY, BTN_SECONDARY, SECTION_TITLE_STYLE, DANGER, TEXT_MAIN, TEXT_SUB, BORDER, ) self.setWindowTitle('Archivia valutazioni') self.setFixedWidth(440) self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) self.setStyleSheet(DIALOG_BG) layout = QVBoxLayout(self) layout.setContentsMargins(24, 24, 24, 20) layout.setSpacing(0) # Titolo title = QLabel('Archivia valutazioni') title.setStyleSheet(f'color: {TEXT_MAIN}; font-size: 15px; font-weight: 700;') layout.addWidget(title) layout.addSpacing(4) # Sottotitolo conteggio righe_txt = f'{n_righe} rig{"a" if n_righe == 1 else "he"} da archiviare' if n_no_imdb: righe_txt += f' · {n_no_imdb} senza IMDB (saltate)' sub = QLabel(righe_txt) sub.setStyleSheet( f'color: {DANGER}; font-size: 12px;' if n_no_imdb else f'color: {TEXT_SUB}; font-size: 12px;' ) layout.addWidget(sub) layout.addSpacing(20) # Separatore sep1 = QFrame(); sep1.setFrameShape(QFrame.Shape.HLine) sep1.setStyleSheet(f'background: {BORDER}; border: none; max-height: 1px;') layout.addWidget(sep1) layout.addSpacing(16) # Sezione fornitore lbl_forn = QLabel('LISTINO / FORNITORE') lbl_forn.setStyleSheet(SECTION_TITLE_STYLE) layout.addWidget(lbl_forn) layout.addSpacing(6) from PyQt6.QtWidgets import QCompleter from PyQt6.QtCore import Qt as _Qt self._combo = QComboBox() self._combo.setEditable(True) self._combo.setStyleSheet(INPUT) self._combo.addItems(fornitori) self._combo.setCurrentText('') self._combo.lineEdit().setPlaceholderText('Seleziona o digita...') completer = QCompleter(fornitori, self._combo) completer.setCaseSensitivity(_Qt.CaseSensitivity.CaseInsensitive) completer.setFilterMode(_Qt.MatchFlag.MatchContains) self._combo.setCompleter(completer) self._combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) layout.addWidget(self._combo) layout.addSpacing(4) hint_forn = QLabel(f'{len(fornitori)} fornitori già presenti — o inserisci un nuovo nome') hint_forn.setStyleSheet(f'color: {TEXT_SUB}; font-size: 11px;') layout.addWidget(hint_forn) layout.addSpacing(18) # Sezione data lbl_data = QLabel('DATA DI ARCHIVIAZIONE') lbl_data.setStyleSheet(SECTION_TITLE_STYLE) layout.addWidget(lbl_data) layout.addSpacing(6) self._edit_data = QLineEdit() self._edit_data.setStyleSheet(INPUT) self._edit_data.setFixedWidth(140) self._edit_data.setPlaceholderText('GG/MM/AAAA') self._edit_data.setText(date.today().strftime('%d/%m/%Y')) layout.addWidget(self._edit_data) layout.addSpacing(4) hint_data = QLabel('Lascia la data odierna o inseriscine una diversa') hint_data.setStyleSheet(f'color: {TEXT_SUB}; font-size: 11px;') layout.addWidget(hint_data) layout.addSpacing(24) # Separatore sep2 = QFrame(); sep2.setFrameShape(QFrame.Shape.HLine) sep2.setStyleSheet(f'background: {BORDER}; border: none; max-height: 1px;') layout.addWidget(sep2) layout.addSpacing(16) # Bottoni btn_row = QHBoxLayout() btn_row.setSpacing(10) btn_row.addStretch() btn_ann = QPushButton('Annulla') btn_ann.setStyleSheet(BTN_SECONDARY) btn_ann.clicked.connect(self.reject) btn_pro = QPushButton('Archivia') btn_pro.setStyleSheet(BTN_PRIMARY) btn_pro.setDefault(True) btn_pro.clicked.connect(self._on_prosegui) btn_row.addWidget(btn_ann) btn_row.addWidget(btn_pro) layout.addLayout(btn_row) def _on_prosegui(self): fornitore = self._combo.currentText().strip() if not fornitore: QMessageBox.warning(self, 'Campo obbligatorio', 'Inserire il nome del listino/fornitore.') self._combo.setFocus() return data_str = self._edit_data.text().strip() try: d = date(int(data_str[6:10]), int(data_str[3:5]), int(data_str[0:2])) self._data_iso = d.strftime('%Y-%m-%d') except (ValueError, IndexError): QMessageBox.warning(self, 'Data non valida', 'Inserire la data nel formato GG/MM/AAAA.') self._edit_data.setFocus() return self._fornitore = fornitore self.accept() def get_fornitore(self) -> str: return getattr(self, '_fornitore', '') def get_data_iso(self) -> str: return getattr(self, '_data_iso', date.today().isoformat()) # ── Foglio principale ────────────────────────────────────────────────────── class ValutazioniRetiSheet(QWidget): """Foglio per inserimento valutazioni reti. app_type = 'valutazioni_reti'.""" app_type = 'valutazioni_reti' def __init__(self, linker_db, parent=None): super().__init__(parent) self._db = linker_db self._wizard_mode = False self._build_ui() # ── UI ──────────────────────────────────────────────────────────── def _build_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layout.addWidget(self._build_toolbar()) # Wizard bar self._wizard_bar = WizardBarValutazioni(self) self._wizard_bar.hide() self._wizard_bar.step_requested.connect(self._on_wizard_step_requested) layout.addWidget(self._wizard_bar) # Hint panel self._wizard_hint = WizardHintPanel(self, hints=_HINTS_VALUTAZIONI) self._wizard_hint.hide() layout.addWidget(self._wizard_hint) # Tabella self.table = _ValTable(self) self.table.setColumnCount(len(_COLUMNS)) self.table.verticalHeader().setMinimumSectionSize(17) self.table.verticalHeader().setDefaultSectionSize(17) self.table.setRowCount(_INITIAL_ROWS) self.table.setHorizontalHeader(_ValHeader(self.table)) self.table.setHorizontalHeaderLabels(_COLUMNS) self.table.verticalHeader().setDefaultSectionSize(17) self.table.verticalHeader().setMinimumWidth(38) self.table.verticalHeader().setFont(QFont('Calibri', 7)) self.table.setFont(_CELL_FONT) self.table.setAlternatingRowColors(True) self.table.setSortingEnabled(True) self.table.setSelectionMode(QTableWidget.SelectionMode.ContiguousSelection) self.table.setEditTriggers( QTableWidget.EditTrigger.DoubleClicked | QTableWidget.EditTrigger.SelectedClicked ) for i, col in enumerate(_COLUMNS): self.table.setColumnWidth(i, _COL_WIDTHS[col]) self.table.verticalHeader().setDefaultSectionSize(17) self.table.setRowCount(0) self.table.setRowCount(_INITIAL_ROWS) layout.addWidget(self.table) self.table.setVerticalHeader(SelectableVHeader(self.table)) hh = self.table.horizontalHeader() hh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) hh.customContextMenuRequested.connect(self._on_hheader_context_menu) vh = self.table.verticalHeader() vh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) vh.customContextMenuRequested.connect(self._on_vheader_context_menu) self.table.selectionModel().selectionChanged.connect( lambda *_: (hh.viewport().update(), vh.viewport().update()) ) def _build_toolbar(self) -> QFrame: toolbar = QFrame() toolbar.setFrameShape(QFrame.Shape.Box) toolbar.setLineWidth(1) toolbar.setFixedHeight(38) toolbar.setStyleSheet(f'background-color: {_TOOLBAR_COLOR};') tbl = QHBoxLayout(toolbar) tbl.setContentsMargins(8, 4, 8, 4) tbl.setSpacing(6) # ≡ menu menu = QMenu(toolbar) act_reset = QAction('Reset foglio', toolbar) act_reset.triggered.connect(self._reset_foglio) menu.addAction(act_reset) menu.addSeparator() act_esporta = QAction('Esporta', toolbar) act_esporta.triggered.connect(self._esporta) menu.addAction(act_esporta) btn_menu = QToolButton(toolbar) btn_menu.setText('≡') btn_menu.setFont(QFont('Calibri', 11)) btn_menu.setFixedHeight(26) btn_menu.setFixedWidth(30) btn_menu.setStyleSheet( 'QToolButton { background-color: #C4B5E8; border: 1px solid #9B7FD4;' ' border-radius: 3px; padding: 0 4px; }' 'QToolButton:hover { background-color: #B3A2DC; }' 'QToolButton::menu-indicator { width: 0; }' ) btn_menu.setMenu(menu) btn_menu.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) tbl.addWidget(btn_menu) # ▶ Verifica IMDB (step 0 → 1) self._btn_verifica = QPushButton('▶ Verifica IMDB') self._btn_verifica.setFont(QFont('Calibri', 9, QFont.Weight.Bold)) self._btn_verifica.setFixedHeight(26) self._btn_verifica.setStyleSheet(_BTN_STYLE) self._btn_verifica.clicked.connect(self._on_verifica) self._btn_verifica.hide() tbl.addWidget(self._btn_verifica) # ▶ Archivia (step 1 → 2) self._btn_procedi = QPushButton('▶ Archivia') self._btn_procedi.setFont(QFont('Calibri', 9, QFont.Weight.Bold)) self._btn_procedi.setFixedHeight(26) self._btn_procedi.setStyleSheet(_BTN_STYLE) self._btn_procedi.clicked.connect(self._on_procedi) self._btn_procedi.hide() tbl.addWidget(self._btn_procedi) # Salva valutazioni (sempre visibile fuori wizard; in wizard appare allo step 2) self._btn_salva = QPushButton('Salva valutazioni') self._btn_salva.setFont(QFont('Calibri', 9, QFont.Weight.Bold)) self._btn_salva.setFixedHeight(26) self._btn_salva.setStyleSheet(_BTN_STYLE) self._btn_salva.clicked.connect(self._on_salva) tbl.addWidget(self._btn_salva) # Label modalità a destra self._mode_label = QLabel('VALUTAZIONI RETI') self._mode_label.setFont(QFont('Calibri', 10, QFont.Weight.Bold)) self._mode_label.setAlignment( Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight ) tbl.addWidget(self._mode_label, stretch=1) return toolbar # ── Wizard API ──────────────────────────────────────────────────── def start_wizard(self): self._wizard_mode = True self._wizard_bar.reset() self._wizard_bar.show() self._wizard_hint.set_step(0) self._btn_verifica.show() self._btn_procedi.hide() self._btn_salva.hide() def _wizard_advance(self, step: int): self._wizard_bar.set_step(step) self._wizard_hint.set_step(step) def _on_wizard_step_requested(self, index: int): if index < self._wizard_bar.current_step(): self._wizard_advance(index) self._sync_wizard_buttons(index) def _sync_wizard_buttons(self, step: int): self._btn_verifica.setVisible(step == 0) self._btn_procedi.setVisible(step == 1) self._btn_salva.setVisible(not self._wizard_mode or step == 2) # ── Step handlers ───────────────────────────────────────────────── def _on_verifica(self): rows = self.get_rows() if not rows: QMessageBox.information(self, 'Nessun dato', 'Non ci sono righe da verificare.') return no_imdb = 0 for r in range(self.table.rowCount()): ha_dati = any( (self.table.item(r, c) or QTableWidgetItem()).text().strip() for c in range(len(_COLUMNS) - 1) ) if not ha_dati: continue imdb_item = self.table.item(r, _COL_IMDB) manca = _imdb_mancante(imdb_item.text() if imdb_item else '') for c in range(len(_COLUMNS)): item = self.table.item(r, c) if item is None: item = QTableWidgetItem() self.table.setItem(r, c, item) if manca: item.setBackground(_BG_NO_IMDB) else: item.setData(Qt.ItemDataRole.BackgroundRole, None) if manca: no_imdb += 1 if self._wizard_mode: self._wizard_advance(1) self._sync_wizard_buttons(1) if no_imdb: QMessageBox.warning( self, 'IMDB mancante', f'{no_imdb} rig{"a" if no_imdb == 1 else "he"} senza codice IMDB ' f'{"è stata evidenziata" if no_imdb == 1 else "sono state evidenziate"} ' f'in rosso.\nCompletale prima di archiviare.' ) def _on_procedi(self): if self._wizard_mode: self._wizard_advance(2) self._sync_wizard_buttons(2) # ── Dati ────────────────────────────────────────────────────────── def get_rows(self) -> list[dict]: result = [] for r in range(self.table.rowCount()): row = {} for c, col in enumerate(_COLUMNS): item = self.table.item(r, c) val = item.text().strip() if item else '' if val: row[col] = val if row: result.append(row) return result def has_data(self) -> bool: for r in range(self.table.rowCount()): for c in range(self.table.columnCount()): item = self.table.item(r, c) if item and item.text().strip(): return True return False def get_rows_data(self) -> list[dict]: result = [] for r in range(self.table.rowCount()): cells = {} for c in range(self.table.columnCount()): item = self.table.item(r, c) if item and item.text().strip(): cells[str(c)] = item.text().strip() if cells: result.append({'row': r, 'cells': cells}) return result def restore_rows_data(self, rows_data: list[dict]): if not rows_data: return max_row = max(d['row'] for d in rows_data) + 1 if max_row > self.table.rowCount(): self.table.verticalHeader().setDefaultSectionSize(17) self.table.setRowCount(max_row) self.table.setSortingEnabled(False) try: for d in rows_data: r = d['row'] for c_str, val in d.get('cells', {}).items(): c = int(c_str) if c < self.table.columnCount(): item = QTableWidgetItem(val) item.setFont(_CELL_FONT) self.table.setItem(r, c, item) finally: self.table.setSortingEnabled(True) def wizard_advance(self, step: int): """API pubblica compatibile con LinkerSheet per il restore della sessione.""" self._wizard_advance(step) self._sync_wizard_buttons(step) # ── Salvataggio ─────────────────────────────────────────────────── def _on_salva(self): rows = self.get_rows() if not rows: QMessageBox.information(self, 'Nessun dato', 'Non ci sono righe da salvare.') return no_imdb = sum(1 for r in rows if _imdb_mancante(r.get('IMDB', ''))) try: fornitori = self._db.get_fornitori_valutazione() except Exception: fornitori = [] dlg = _SalvaDialog(fornitori, len(rows), no_imdb, parent=self) if dlg.exec() != QDialog.DialogCode.Accepted: return total_to_save = len(rows) - no_imdb progress = _SalvaProgressDialog(total_to_save, parent=self) worker = _SalvaWorker(self._db, rows, dlg.get_fornitore(), dlg.get_data_iso(), parent=self) worker.chunk_done.connect(progress.on_chunk) worker.finished.connect(progress.on_finished) worker.error.connect(progress.on_error) worker.finished.connect(lambda *_: self._post_salva()) worker.start() progress.exec() def _post_salva(self): if self._wizard_mode: self._wizard_advance(2) self._sync_wizard_buttons(2) # ── Menu ────────────────────────────────────────────────────────── def _reset_foglio(self): r = QMessageBox.question(self, 'Reset foglio', 'Cancellare tutti i dati inseriti?', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) if r == QMessageBox.StandardButton.Yes: self.table.verticalHeader().setDefaultSectionSize(17) self.table.setRowCount(0) self.table.setRowCount(_INITIAL_ROWS) def _on_hheader_context_menu(self, pos): col = self.table.horizontalHeader().logicalIndexAt(pos) if col < 0: return hh = self.table.horizontalHeader() sel_cols = hh.get_selected_cols() if not sel_cols: sel_cols = [col] hi = self.table.horizontalHeaderItem(sel_cols[0]) label = (f'Copia colonna "{hi.text() if hi else ""}"' if len(sel_cols) == 1 else f'Copia {len(sel_cols)} colonne') menu = QMenu(self) act = menu.addAction(label) if menu.exec(hh.mapToGlobal(pos)) == act: self._copy_columns_hdr(sel_cols) def _on_vheader_context_menu(self, pos): row = self.table.verticalHeader().logicalIndexAt(pos) if row < 0: return sel_rows = sorted({i.row() for i in self.table.selectedIndexes()}) if not sel_rows: sel_rows = [row] label = 'Copia riga' if len(sel_rows) == 1 else f'Copia {len(sel_rows)} righe' menu = QMenu(self) act = menu.addAction(label) if menu.exec(self.table.verticalHeader().mapToGlobal(pos)) == act: self._copy_rows_hdr(sel_rows) def _copy_columns_hdr(self, cols: list[int]): hi = self.table.horizontalHeaderItem header = '\t'.join((hi(c).text() if hi(c) else '') for c in cols) lines = [header] for r in range(self.table.rowCount()): if self.table.isRowHidden(r): continue cells = [(self.table.item(r, c).text() if self.table.item(r, c) else '') for c in cols] lines.append('\t'.join(cells)) QApplication.clipboard().setText('\n'.join(lines)) def _copy_rows_hdr(self, rows: list[int]): lines = [] for r in rows: cells = [(self.table.item(r, c).text() if self.table.item(r, c) else '') for c in range(self.table.columnCount())] lines.append('\t'.join(cells)) QApplication.clipboard().setText('\n'.join(lines)) def _esporta(self): rows = self.get_rows() if not rows: QMessageBox.information(self, 'Nessun dato', 'Non ci sono righe da esportare.') return from PyQt6.QtWidgets import QFileDialog path, _ = QFileDialog.getSaveFileName( self, 'Esporta valutazioni', 'valutazioni_reti.xlsx', 'Excel (*.xlsx)' ) if not path: return try: import openpyxl from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter _side = Side(style='thin', color='A0A0A0') _border = Border(left=_side, right=_side, top=_side, bottom=_side) _cell_font = Font(name='Calibri', size=8) _hdr_font = Font(name='Calibri', size=8, bold=True) _center = Alignment(horizontal='center', vertical='center') _hdr_align = Alignment(horizontal='center', vertical='center', wrap_text=True) wb = openpyxl.Workbook() ws = wb.active ws.title = 'Valutazioni Reti' # Intestazione — giallo per val, ciano per IMDB (come _ValHeader) ws.row_dimensions[1].height = round(33 * 0.75, 2) for ci, col_name in enumerate(_COLUMNS, start=1): cell = ws.cell(row=1, column=ci, value=col_name) cell.font = _hdr_font cell.fill = PatternFill('solid', fgColor='00BFFF' if col_name == 'IMDB' else 'FFFF00') cell.alignment = _hdr_align cell.border = _border px = self.table.columnWidth(ci - 1) ws.column_dimensions[get_column_letter(ci)].width = max(4.0, round(px / 7, 2)) # Dati _row_h = round(17 * 0.75, 2) for ri, row in enumerate(rows, start=2): ws.row_dimensions[ri].height = _row_h for ci, col_name in enumerate(_COLUMNS, start=1): cell = ws.cell(row=ri, column=ci, value=row.get(col_name, '')) cell.font = _cell_font cell.alignment = _center cell.border = _border ws.freeze_panes = 'A2' ws.auto_filter.ref = ws.dimensions wb.save(path) QMessageBox.information(self, 'Esportato', f'File salvato:\n{path}') except Exception as e: QMessageBox.critical(self, 'Errore', f'Errore esportazione:\n{e}')