# Comparativo Preventivi — Refactoring v2 Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Riscrivere `ComparativoPreventivi.py` per gestire correttamente file Excel eterogenei delle imprese (righe extra in testa e intermedie), permettere inserimento manuale del nome azienda nella GUI, riorganizzare le colonne del comparativo (tutti i P.U. prima, poi tutti i Totali), e modernizzare l'interfaccia utente.

**Architecture:** Il file rimane monolitico (un solo `.py`). La logica Excel viene separata in funzioni più robuste: `trova_riga_intestazione()` per il rilevamento automatico dell'inizio dati, `leggi_preventivo_v2()` che restituisce sia i prezzi che le note extra intermedie. La GUI viene ridisegnata con un look moderno (palette scura/accent, card layout, animazioni semplici via ttk) mantenendo tkinter come unico framework.

**Tech Stack:** Python 3.8+, tkinter (ttk), openpyxl

---

## Contesto critico per chi implementa

Il file attuale `ComparativoPreventivi.py` (695 righe) funziona ma ha 5 criticità da risolvere:

1. **Fedeltà capitolato**: già ok strutturalmente, si garantisce con i fix successivi
2. **Righe extra in testa**: `leggi_preventivo` parte da riga 3 hardcoded → bisogna trovare dinamicamente la riga di intestazione
3. **Nome azienda dalla GUI**: attualmente estratto dal file → ora l'utente lo digita in un campo nella lista preventivi
4. **Layout colonne**: attualmente `[PU_1, TOT_1, PU_2, TOT_2]` → deve diventare `[PU_1, PU_2, TOT_1, TOT_2]`
5. **Note extra intermedie**: righe con numero voce non presente nel capitolato → vanno rilevate e inserite nel comparativo finale per quella specifica impresa

**Struttura dati chiave post-refactoring:**

```python
# Struttura impresa (attuale)
{"nome": str, "voci": dict{num: prezzo}, "file": str}

# Struttura impresa (nuova)
{
    "nome": str,                    # inserito dall'utente nella GUI
    "voci": dict{num: float},       # num_voce → prezzo_unitario
    "note_extra": list[dict],       # [{dopo_num: str, testo: str, riga_orig: int}]
    "file": str
}
```

**Rilevamento intestazione:**

```python
# Il capitolato ha sempre una riga di intestazione (es. riga 2) con testi tipo:
# Col A: "N°" o "Num" o "Voce"
# Col B: "Descrizione" o "Lavorazione"
# Col C: "U.M." o "UM"
# Col D: "Quantità" o "Qta"
# Col E: "Prezzo" o "P.U."

# Il sistema:
# 1. Estrae la "firma" dell'intestazione dal capitolato
# 2. Nel file impresa, scansiona riga per riga calcolando match con la firma
# 3. La prima riga con >= 2 match (su 5 colonne, case-insensitive) è l'intestazione
# 4. I dati iniziano dalla riga successiva
```

**Rilevamento note extra:**

```python
# Dopo aver trovato l'intestazione nel file impresa, legge riga per riga.
# Per ogni riga:
#   - Se col A contiene un numero presente nel capitolato → voce normale
#   - Se col A è vuota/None o contiene testo non presente nel capitolato → nota extra
#     → viene associata all'ultimo num_voce del capitolato visto prima
```

**Nuovo layout colonne nel foglio Comparativo:**

```
Col 1: N°
Col 2: Descrizione
Col 3: U.M.
Col 4: Quantità
Col 5..5+N-1: P.U. impresa 1, P.U. impresa 2, ..., P.U. impresa N
Col 5+N: Media P.U.
Col 5+N+1..5+2N: Totale impresa 1, Totale impresa 2, ..., Totale impresa N
Col 5+2N+1: Note anomalie
```

Le formule Totale diventano `=D_row * PU_col_row` dove `PU_col` è la colonna del P.U. della stessa impresa.

---

## Task 1: Aggiungere `trova_riga_intestazione()` e `estrai_firma_intestazione()`

**Files:**
- Modify: `ComparativoPreventivi.py` (dopo la funzione `leggi_capitolato`, circa riga 103)

**Step 1: Aggiungere `estrai_firma_intestazione()`**

Questa funzione legge il capitolato e restituisce i testi delle celle di intestazione (riga 2, colonne A-E) normalizzati (lowercase, strip).

```python
def estrai_firma_intestazione(capitolato_path):
    """
    Legge la riga di intestazione del capitolato (riga 2) e restituisce
    una lista di token normalizzati per il confronto con i file imprese.
    """
    wb = load_workbook(capitolato_path, data_only=True)
    ws = wb.active
    firma = []
    for row in ws.iter_rows(min_row=2, max_row=2, max_col=6, values_only=True):
        for val in row:
            token = str(val).strip().lower() if val else ""
            firma.append(token)
    wb.close()
    return firma  # lista di 6 stringhe (una per colonna A-F)
```

**Step 2: Aggiungere `trova_riga_intestazione()`**

```python
def trova_riga_intestazione(ws, firma_capitolato, soglia_match=2):
    """
    Scansiona il worksheet ws cercando la riga che meglio corrisponde
    all'intestazione del capitolato. Restituisce il numero di riga (1-based)
    dell'intestazione trovata, oppure None se non trovata.

    Logica:
    - Per ogni riga, confronta i valori delle prime 6 colonne con firma_capitolato
    - Conta quanti valori corrispondono (case-insensitive, strip, substring match)
    - Restituisce la prima riga con >= soglia_match match
    """
    for row in ws.iter_rows(max_col=6):
        match_count = 0
        for idx, cell in enumerate(row):
            if idx >= len(firma_capitolato):
                break
            firma_token = firma_capitolato[idx]
            if not firma_token:
                continue
            cell_val = str(cell.value).strip().lower() if cell.value else ""
            # substring match: "descrizione lavorazione" contiene "descrizione"
            if firma_token in cell_val or cell_val in firma_token:
                match_count += 1
        if match_count >= soglia_match:
            return row[0].row  # numero riga (1-based)
    return None
```

**Step 3: Verificare a mano che le funzioni siano sintatticamente corrette** — eseguire:

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import ast, sys
with open('ComparativoPreventivi.py') as f:
    src = f.read()
try:
    ast.parse(src)
    print('OK: sintassi corretta')
except SyntaxError as e:
    print(f'ERRORE: {e}')
    sys.exit(1)
"
```

Risultato atteso: `OK: sintassi corretta`

---

## Task 2: Riscrivere `leggi_preventivo()` → `leggi_preventivo_v2()`

**Files:**
- Modify: `ComparativoPreventivi.py` (sostituire la funzione `leggi_preventivo`, righe 52-79)

**Step 1: Sostituire `leggi_preventivo` con la nuova versione**

La nuova funzione accetta anche `firma_capitolato` e `numeri_voce_capitolato` per il rilevamento automatico e il riconoscimento note extra.

```python
def leggi_preventivo_v2(filepath, firma_capitolato, numeri_voce_capitolato):
    """
    Legge un preventivo Excel compilato da un'impresa.

    Gestisce:
    - Righe extra in testa (intestazione aziendale): trova automaticamente
      la riga di intestazione confrontandola con la firma del capitolato
    - Note extra intermedie: righe con numero voce non presente nel capitolato
      vengono raccolte come note extra associate all'ultima voce vista

    Args:
        filepath: percorso del file Excel dell'impresa
        firma_capitolato: lista di token (da estrai_firma_intestazione)
        numeri_voce_capitolato: set di numeri voce del capitolato (es. {"1.1", "1.2", ...})

    Returns:
        (voci, note_extra)
        voci: dict{num_voce: prezzo_float}
        note_extra: list[{"dopo_num": str, "testo": str}]
    """
    wb = load_workbook(filepath, data_only=True)
    ws = wb.active

    # 1. Trova la riga di intestazione
    riga_intestazione = trova_riga_intestazione(ws, firma_capitolato)
    if riga_intestazione is None:
        # Fallback: assumiamo riga 2 come da struttura originale
        riga_intestazione = 2

    # 2. Determina quale colonna contiene i prezzi unitari
    # Cerca "prezzo" o "p.u." nell'intestazione trovata
    col_prezzo = 4  # default: colonna E (index 4, 0-based)
    for row in ws.iter_rows(min_row=riga_intestazione, max_row=riga_intestazione,
                             max_col=10, values_only=True):
        for idx, val in enumerate(row):
            if val and any(k in str(val).lower() for k in ["prezzo", "p.u.", "unitario", "unit"]):
                col_prezzo = idx
                break

    # 3. Leggi i dati partendo dalla riga successiva all'intestazione
    voci = {}
    note_extra = []
    ultimo_num_capitolato = None

    for row in ws.iter_rows(min_row=riga_intestazione + 1, values_only=True):
        if not row or len(row) < 2:
            continue
        num_raw = row[0]
        desc_raw = row[1] if len(row) > 1 else None
        prezzo_raw = row[col_prezzo] if len(row) > col_prezzo else None

        # Riga completamente vuota: salta
        if num_raw is None and desc_raw is None:
            continue

        num_str = str(num_raw).strip() if num_raw is not None else ""

        if num_str and num_str in numeri_voce_capitolato:
            # Voce normale del capitolato
            try:
                voci[num_str] = float(prezzo_raw) if prezzo_raw is not None else None
            except (ValueError, TypeError):
                voci[num_str] = None
            ultimo_num_capitolato = num_str
        else:
            # Nota extra dell'impresa (riga con num non presente nel capitolato)
            # Cattura il testo da tutte le colonne non vuote
            testo_parti = []
            for val in row:
                if val is not None and str(val).strip():
                    testo_parti.append(str(val).strip())
            testo = " — ".join(testo_parti) if testo_parti else ""
            if testo:
                note_extra.append({
                    "dopo_num": ultimo_num_capitolato,
                    "testo": testo
                })

    wb.close()
    return voci, note_extra
```

**Step 2: Verificare sintassi**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import ast, sys
with open('ComparativoPreventivi.py') as f:
    src = f.read()
try:
    ast.parse(src)
    print('OK: sintassi corretta')
except SyntaxError as e:
    print(f'ERRORE: {e}')
    sys.exit(1)
"
```

---

## Task 3: Aggiornare `genera_comparativo()` per usare la nuova logica

**Files:**
- Modify: `ComparativoPreventivi.py` (funzione `genera_comparativo`, righe 106-145)

**Step 1: Aggiornare `genera_comparativo()`**

La funzione ora:
- Accetta `nomi_imprese` (lista di stringhe) oltre ai path
- Usa `estrai_firma_intestazione` e `leggi_preventivo_v2`
- Passa `note_extra` alla struttura impresa

```python
def genera_comparativo(capitolato_path, preventivi_paths, nomi_imprese,
                       output_path, log_fn=None):
    """
    Args:
        capitolato_path: path del file Excel di riferimento
        preventivi_paths: lista di path dei preventivi imprese
        nomi_imprese: lista di nomi imprese (stesso ordine di preventivi_paths)
        output_path: path del file Excel di output
        log_fn: funzione di log opzionale
    """
    def log(msg):
        if log_fn:
            log_fn(msg)

    log("📋 Lettura capitolato...")
    struttura = leggi_capitolato(capitolato_path)
    voci_struttura = [s for s in struttura if s["tipo"] == "voce"]
    numeri_voce = {v["num"] for v in voci_struttura}
    log(f"   → {len(voci_struttura)} voci trovate")

    log("🔍 Estrazione firma intestazione...")
    firma = estrai_firma_intestazione(capitolato_path)

    imprese = []
    for path, nome in zip(preventivi_paths, nomi_imprese):
        nome_file = os.path.basename(path)
        log(f"🏗  Lettura: {nome_file}")
        voci, note_extra = leggi_preventivo_v2(path, firma, numeri_voce)
        imprese.append({
            "nome": nome,
            "voci": voci,
            "note_extra": note_extra,
            "file": path
        })
        compilate = sum(1 for v in voci_struttura if voci.get(v["num"]) is not None)
        n_note = len(note_extra)
        log(f"   → {nome}: {compilate} voci prezzate, "
            f"{len(voci_struttura)-compilate} mancanti, {n_note} note extra")

    n_imprese = len(imprese)

    # Calcola medie (esclude None e 0)
    medie = {}
    for voce in voci_struttura:
        num = voce["num"]
        prezzi_validi = [
            imp["voci"].get(num)
            for imp in imprese
            if imp["voci"].get(num) is not None and imp["voci"].get(num) > 0
        ]
        medie[num] = sum(prezzi_validi) / len(prezzi_validi) if prezzi_validi else None

    log("📊 Generazione Excel...")
    wb_out = Workbook()
    ws = wb_out.active
    ws.title = "Comparativo"
    _genera_foglio_comparativo(ws, struttura, imprese, medie, n_imprese)
    ws_r = wb_out.create_sheet("Riepilogo")
    _genera_foglio_riepilogo(ws_r, struttura, imprese, medie, n_imprese)

    wb_out.save(output_path)
    log(f"✅ File salvato: {os.path.basename(output_path)}")
    return output_path
```

**Step 2: Verificare sintassi**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import ast, sys
with open('ComparativoPreventivi.py') as f:
    src = f.read()
try:
    ast.parse(src)
    print('OK: sintassi corretta')
except SyntaxError as e:
    print(f'ERRORE: {e}')
    sys.exit(1)
"
```

---

## Task 4: Riscrivere `_genera_foglio_comparativo()` con nuovo layout colonne e note extra

**Files:**
- Modify: `ComparativoPreventivi.py` (funzione `_genera_foglio_comparativo`, righe 148-357)

**Step 1: Sostituire con la nuova versione**

Il nuovo layout è:
```
[N°][Desc][UM][QTA] | [PU_1][PU_2]...[PU_N] | [MEDIA_PU] | [TOT_1][TOT_2]...[TOT_N] | [NOTE]
```

Le note extra dell'impresa vengono inserite come righe aggiuntive dopo la voce del capitolato a cui appartengono, con celle unite e sfondo grigio chiaro.

```python
def _genera_foglio_comparativo(ws, struttura, imprese, medie, n_imprese):
    thin = Side(style="thin", color="BFBFBF")
    thick = Side(style="medium", color="2E75B6")
    b = Border(left=thin, right=thin, top=thin, bottom=thin)
    bs = Border(left=thick, right=thick, top=thick, bottom=thick)
    def bg(c): return PatternFill("solid", start_color=c)
    def alg(h="center", v="center", wrap=False, indent=0):
        return Alignment(horizontal=h, vertical=v, wrap_text=wrap, indent=indent)

    # Layout colonne:
    # 1..4: N°, Desc, UM, QTA
    # 5..4+N: PU impresa 1..N
    # 4+N+1: Media PU
    # 4+N+2..4+2N+1: Totale impresa 1..N
    # 4+2N+2: Note anomalie
    base_cols = 4
    col_pu_start = base_cols + 1                        # prima colonna P.U.
    col_media = base_cols + n_imprese + 1               # colonna Media
    col_tot_start = col_media + 1                       # prima colonna Totale
    col_note = col_tot_start + n_imprese                # colonna Note
    last_col = get_column_letter(col_note)

    # Larghezze colonne
    ws.column_dimensions["A"].width = 8
    ws.column_dimensions["B"].width = 52
    ws.column_dimensions["C"].width = 9
    ws.column_dimensions["D"].width = 11
    for i in range(n_imprese):
        ws.column_dimensions[get_column_letter(col_pu_start + i)].width = 14
    ws.column_dimensions[get_column_letter(col_media)].width = 14
    for i in range(n_imprese):
        ws.column_dimensions[get_column_letter(col_tot_start + i)].width = 14
    ws.column_dimensions[get_column_letter(col_note)].width = 32

    # ── Riga 1: Titolo ──
    ws.row_dimensions[1].height = 28
    ws.merge_cells(f"A1:{last_col}1")
    c = ws["A1"]
    c.value = f"COMPARATIVO OFFERTE — {datetime.now().strftime('%d/%m/%Y %H:%M')}"
    c.font = Font(name=FONT_NAME, bold=True, size=12, color="FFFFFF")
    c.fill = bg(COL_HEADER_BG)
    c.alignment = alg("left", indent=2)

    # ── Riga 2: Legenda ──
    ws.row_dimensions[2].height = 16
    ws.merge_cells(f"A2:{last_col}2")
    c = ws["A2"]
    c.value = (f"Soglia anomalie: ±{SOGLIA_ANOMALIA_PERC}%  |  "
               "ROSSO = prezzo alto   VERDE = prezzo basso   GIALLO = voce mancante (media)")
    c.font = Font(name=FONT_NAME, size=8, italic=True, color="404040")
    c.fill = bg("D6E4F0")
    c.alignment = alg("left", indent=2)

    # ── Riga 3: Nomi imprese (group header) ──
    ws.row_dimensions[3].height = 18
    # Sfondo intestazioni fisse
    for col in range(1, base_cols + 1):
        ws.cell(row=3, column=col).fill = bg(COL_HEADER_BG)
        ws.cell(row=3, column=col).border = b

    # Header gruppo "PREZZI UNITARI"
    col_pu_end = col_pu_start + n_imprese - 1
    ws.merge_cells(f"{get_column_letter(col_pu_start)}3:{get_column_letter(col_pu_end)}3")
    c = ws.cell(row=3, column=col_pu_start, value="PREZZI UNITARI (€)")
    c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill = bg(COL_SECTION_BG)
    c.alignment = alg()
    for col in range(col_pu_start, col_pu_end + 1):
        ws.cell(row=3, column=col).border = b

    # Media
    c = ws.cell(row=3, column=col_media, value="")
    c.fill = bg("375623")
    c.border = b

    # Header gruppo "TOTALI"
    col_tot_end = col_tot_start + n_imprese - 1
    ws.merge_cells(f"{get_column_letter(col_tot_start)}3:{get_column_letter(col_tot_end)}3")
    c = ws.cell(row=3, column=col_tot_start, value="TOTALI (€)")
    c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill = bg(COL_SECTION_BG)
    c.alignment = alg()
    for col in range(col_tot_start, col_tot_end + 1):
        ws.cell(row=3, column=col).border = b

    # Note
    c = ws.cell(row=3, column=col_note, value="")
    c.fill = bg(COL_HEADER_BG)
    c.border = b

    # ── Riga 4: Intestazioni colonne ──
    ws.row_dimensions[4].height = 30
    for col, h in enumerate(["N°", "DESCRIZIONE LAVORAZIONE", "U.M.", "QUANTITÀ"], 1):
        c = ws.cell(row=4, column=col, value=h)
        c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill = bg(COL_HEADER_BG)
        c.alignment = alg(wrap=True)
        c.border = b

    # P.U. per impresa
    for i, imp in enumerate(imprese):
        c = ws.cell(row=4, column=col_pu_start + i, value=imp["nome"].upper())
        c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill = bg(COL_SECTION_BG)
        c.alignment = alg(wrap=True)
        c.border = b

    # Media P.U.
    c = ws.cell(row=4, column=col_media, value="MEDIA P.U.")
    c.font = Font(name=FONT_NAME, bold=True, size=9, color="1F4E79")
    c.fill = bg(COL_MEDIA_BG)
    c.alignment = alg(wrap=True)
    c.border = b

    # Totali per impresa
    for i, imp in enumerate(imprese):
        c = ws.cell(row=4, column=col_tot_start + i, value=imp["nome"].upper())
        c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill = bg(COL_SECTION_BG)
        c.alignment = alg(wrap=True)
        c.border = b

    # Note
    c = ws.cell(row=4, column=col_note, value="NOTE ANOMALIE")
    c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill = bg(COL_HEADER_BG)
    c.alignment = alg(wrap=True)
    c.border = b

    ws.freeze_panes = "A5"
    current_row = 5
    totale_rows = []  # righe con voci (per formula totale finale)

    # Costruisci dict: num_voce → lista di note extra per impresa
    # {num_voce: {idx_impresa: [testo, ...]}}
    note_per_voce = {}
    for idx_imp, imp in enumerate(imprese):
        for nota in imp.get("note_extra", []):
            dopo = nota["dopo_num"]
            if dopo not in note_per_voce:
                note_per_voce[dopo] = {}
            if idx_imp not in note_per_voce[dopo]:
                note_per_voce[dopo][idx_imp] = []
            note_per_voce[dopo][idx_imp].append(nota["testo"])

    for voce in struttura:
        if voce["tipo"] == "sezione":
            # Riga sezione: celle unite, sfondo blu medio
            ws.merge_cells(f"A{current_row}:{last_col}{current_row}")
            c = ws.cell(row=current_row, column=1, value=voce["desc"])
            c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
            c.fill = bg(COL_SECTION_BG)
            c.alignment = Alignment(horizontal="left", vertical="center", indent=2)
            c.border = bs
            ws.row_dimensions[current_row].height = 18
            current_row += 1
            continue

        num = voce["num"]
        qta = voce["qta"]
        media = medie.get(num)

        # Altezza riga adattiva
        desc_text = voce["desc"] or ""
        n_lines = max(1, -(-len(desc_text) // 70))
        ws.row_dimensions[current_row].height = max(16, min(n_lines * 13, 72))

        # Colonne fisse A-D
        for col, val in enumerate([num, desc_text, voce["um"], qta], 1):
            c = ws.cell(row=current_row, column=col, value=val)
            c.font = Font(name=FONT_NAME, size=9)
            c.border = b
            if col == 1:
                c.alignment = alg("center")
            elif col == 2:
                c.alignment = Alignment(horizontal="left", vertical="top", wrap_text=True)
            elif col == 3:
                c.alignment = alg("center")
            elif col == 4:
                c.number_format = "#,##0.00"
                c.alignment = alg("right")

        # Prezzi unitari (colonne 5..4+N)
        anomalie_note = []
        for i, imp in enumerate(imprese):
            prezzo = imp["voci"].get(num)
            mancante = prezzo is None or prezzo == 0
            prezzo_display = media if (mancante and media) else (prezzo or 0)

            is_alto = is_basso = False
            if media and media > 0 and not mancante and prezzo_display:
                delta = (prezzo_display - media) / media * 100
                if delta > SOGLIA_ANOMALIA_PERC:
                    is_alto = True
                    anomalie_note.append(f"{imp['nome']}: +{delta:.0f}% (ALTO)")
                elif delta < -SOGLIA_ANOMALIA_PERC:
                    is_basso = True
                    anomalie_note.append(f"{imp['nome']}: {delta:.0f}% (BASSO)")

            c_pu = ws.cell(row=current_row, column=col_pu_start + i, value=prezzo_display)
            c_pu.font = Font(name=FONT_NAME, size=9,
                             bold=(is_alto or is_basso),
                             italic=mancante,
                             color="C00000" if is_alto else
                                   ("375623" if is_basso else
                                    ("7F7F7F" if mancante else "000000")))
            c_pu.number_format = "#,##0.00"
            c_pu.alignment = alg("right")
            c_pu.border = b
            c_pu.fill = bg(COL_MANCANTE_BG if mancante else
                           (COL_ANOMALO_ALTO if is_alto else
                            (COL_ANOMALO_BASSO if is_basso else "FFFFFF")))

        # Media P.U.
        c_med = ws.cell(row=current_row, column=col_media,
                        value=media if media else "—")
        c_med.font = Font(name=FONT_NAME, size=9, bold=True, color="375623")
        c_med.fill = bg(COL_MEDIA_BG)
        if media:
            c_med.number_format = "#,##0.00"
        c_med.alignment = alg("right")
        c_med.border = b

        # Totali (colonne col_tot_start..col_tot_start+N-1)
        # Formula: =D_row * PU_col_impresa_row
        for i in range(n_imprese):
            pu_col_letter = get_column_letter(col_pu_start + i)
            c_tot = ws.cell(row=current_row, column=col_tot_start + i,
                            value=f"=D{current_row}*{pu_col_letter}{current_row}")
            c_tot.font = Font(name=FONT_NAME, size=9)
            c_tot.number_format = "€#,##0.00"
            c_tot.alignment = alg("right")
            c_tot.border = b
            imp = imprese[i]
            prezzo = imp["voci"].get(num)
            mancante = prezzo is None or prezzo == 0
            c_tot.fill = bg(COL_MANCANTE_BG if mancante else "F8F8F8")

        # Note anomalie
        note_text = " | ".join(anomalie_note) if anomalie_note else ""
        c_note = ws.cell(row=current_row, column=col_note, value=note_text)
        c_note.font = Font(name=FONT_NAME, size=8, italic=True,
                           color="C00000" if anomalie_note else "7F7F7F")
        c_note.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
        c_note.border = b

        totale_rows.append(current_row)
        current_row += 1

        # ── Note extra delle imprese dopo questa voce ──
        if num in note_per_voce:
            for idx_imp, testi in note_per_voce[num].items():
                for testo in testi:
                    # Riga nota extra: tutta grigia, testo corsivo
                    ws.row_dimensions[current_row].height = 14
                    # Colonne A-D: unite, sfondo grigio chiaro
                    ws.merge_cells(f"A{current_row}:D{current_row}")
                    c = ws.cell(row=current_row, column=1,
                                value=f"↳ [{imprese[idx_imp]['nome']}] {testo}")
                    c.font = Font(name=FONT_NAME, size=8, italic=True, color="595959")
                    c.fill = bg("F2F2F2")
                    c.alignment = Alignment(horizontal="left", vertical="center", indent=2)
                    c.border = b
                    # Restanti colonne: sfondo grigio, vuote
                    for col in range(base_cols + 1, col_note + 1):
                        cell = ws.cell(row=current_row, column=col)
                        cell.fill = bg("F2F2F2")
                        cell.border = b
                    current_row += 1

    # ── Riga totali finali ──
    current_row += 1
    ws.row_dimensions[current_row].height = 26
    ws.merge_cells(f"A{current_row}:D{current_row}")
    c = ws.cell(row=current_row, column=1, value="TOTALE OFFERTA (IVA ESCLUSA)")
    c.font = Font(name=FONT_NAME, bold=True, size=10, color="FFFFFF")
    c.fill = bg(COL_HEADER_BG)
    c.alignment = alg("right")
    c.border = b

    # P.U. totali: dash
    for i in range(n_imprese):
        c = ws.cell(row=current_row, column=col_pu_start + i, value="—")
        c.font = Font(name=FONT_NAME, bold=True, color="FFFFFF")
        c.fill = bg(COL_HEADER_BG)
        c.alignment = alg()
        c.border = b

    # Media: dash
    c = ws.cell(row=current_row, column=col_media, value="—")
    c.font = Font(name=FONT_NAME, bold=True, color="FFFFFF")
    c.fill = bg(COL_HEADER_BG)
    c.border = b
    c.alignment = alg()

    # Totali finali per impresa
    for i in range(n_imprese):
        tot_col = get_column_letter(col_tot_start + i)
        sum_formula = "+".join([f"{tot_col}{r}" for r in totale_rows])
        c_tot = ws.cell(row=current_row, column=col_tot_start + i,
                        value=f"={sum_formula}")
        c_tot.font = Font(name=FONT_NAME, bold=True, size=10, color="FFFFFF")
        c_tot.fill = bg(COL_HEADER_BG)
        c_tot.number_format = "€#,##0.00"
        c_tot.alignment = alg("right")
        c_tot.border = b

    # Note: vuota
    c = ws.cell(row=current_row, column=col_note, value="")
    c.fill = bg(COL_HEADER_BG)
    c.border = b
```

**Step 2: Verificare sintassi**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import ast, sys
with open('ComparativoPreventivi.py') as f:
    src = f.read()
try:
    ast.parse(src)
    print('OK: sintassi corretta')
except SyntaxError as e:
    print(f'ERRORE: {e}')
    sys.exit(1)
"
```

---

## Task 5: Aggiornare `_genera_foglio_riepilogo()` per il nuovo layout

**Files:**
- Modify: `ComparativoPreventivi.py` (funzione `_genera_foglio_riepilogo`, righe 359-440)

**Step 1: Aggiornare la funzione**

Il riepilogo non cambia logicamente ma deve usare i prezzi dalla struttura impresa aggiornata (che ora usa `voci.get(num)` che può essere `None` invece di `0`).

```python
def _genera_foglio_riepilogo(ws_r, struttura, imprese, medie, n_imprese):
    def bg(c): return PatternFill("solid", start_color=c)
    thin = Side(style="thin", color="BFBFBF")
    border = Border(left=thin, right=thin, top=thin, bottom=thin)
    def alg(h="center"): return Alignment(horizontal=h, vertical="center")

    n_cols = n_imprese + 2  # sezione + N imprese + scarto
    last_col_letter = get_column_letter(n_cols)

    ws_r.column_dimensions["A"].width = 38
    for i in range(1, n_imprese + 2):
        ws_r.column_dimensions[get_column_letter(i + 1)].width = 18

    # Titolo
    ws_r.merge_cells(f"A1:{last_col_letter}1")
    c = ws_r.cell(row=1, column=1, value="RIEPILOGO PER SEZIONE")
    c.font = Font(name=FONT_NAME, bold=True, size=12, color="FFFFFF")
    c.fill = bg(COL_HEADER_BG)
    c.alignment = alg("left")

    # Intestazione
    ws_r.row_dimensions[2].height = 25
    c = ws_r.cell(row=2, column=1, value="SEZIONE")
    c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
    c.fill = bg(COL_HEADER_BG)
    c.alignment = alg("left")
    c.border = border
    for i, imp in enumerate(imprese):
        c = ws_r.cell(row=2, column=i + 2, value=imp["nome"].upper())
        c.font = Font(name=FONT_NAME, bold=True, size=9, color="FFFFFF")
        c.fill = bg(COL_SECTION_BG)
        c.alignment = alg()
        c.border = border
    c = ws_r.cell(row=2, column=n_imprese + 2, value="SCARTO MAX-MIN")
    c.font = Font(name=FONT_NAME, bold=True, size=9, color="1F4E79")
    c.fill = bg(COL_MEDIA_BG)
    c.alignment = alg()
    c.border = border

    # Calcolo totali per sezione
    sezione_corrente = "Generale"
    totali_sezione = {s["desc"]: {imp["nome"]: 0.0 for imp in imprese}
                      for s in struttura if s["tipo"] == "sezione"}
    totali_sezione["TOTALE GENERALE"] = {imp["nome"]: 0.0 for imp in imprese}

    for voce in struttura:
        if voce["tipo"] == "sezione":
            sezione_corrente = voce["desc"]
        elif voce["tipo"] == "voce":
            num, qta = voce["num"], voce["qta"]
            for imp in imprese:
                prezzo = imp["voci"].get(num)
                if prezzo is None or prezzo == 0:
                    prezzo = medie.get(num) or 0
                totale_voce = qta * prezzo
                if sezione_corrente in totali_sezione:
                    totali_sezione[sezione_corrente][imp["nome"]] += totale_voce
                totali_sezione["TOTALE GENERALE"][imp["nome"]] += totale_voce

    current_row = 3
    for sezione_nome, totali_imp in totali_sezione.items():
        is_totale = sezione_nome == "TOTALE GENERALE"
        fill_c = COL_HEADER_BG if is_totale else ("F2F2F2" if current_row % 2 == 0 else "FFFFFF")
        font_c = "FFFFFF" if is_totale else "000000"
        ws_r.row_dimensions[current_row].height = 18
        c = ws_r.cell(row=current_row, column=1, value=sezione_nome)
        c.font = Font(name=FONT_NAME, bold=is_totale, size=9, color=font_c)
        c.fill = bg(fill_c)
        c.alignment = Alignment(horizontal="left", vertical="center", indent=1)
        c.border = border
        valori = []
        for i, imp in enumerate(imprese):
            val = totali_imp[imp["nome"]]
            valori.append(val)
            c = ws_r.cell(row=current_row, column=i + 2, value=val)
            c.font = Font(name=FONT_NAME, bold=is_totale, size=9, color=font_c)
            c.fill = bg(fill_c)
            c.number_format = "€#,##0.00"
            c.alignment = alg("right")
            c.border = border
        diff = max(valori) - min(valori) if len(valori) >= 2 else 0
        diff_pct = diff / min(valori) * 100 if len(valori) >= 2 and min(valori) > 0 else 0
        diff_text = f"€{diff:,.0f}  ({diff_pct:.1f}%)"
        c = ws_r.cell(row=current_row, column=n_imprese + 2, value=diff_text)
        c.font = Font(name=FONT_NAME, bold=is_totale, size=9,
                      color=font_c if is_totale else ("C00000" if diff_pct > 15 else "000000"))
        c.fill = bg(fill_c if is_totale else COL_MEDIA_BG)
        c.alignment = alg("right")
        c.border = border
        current_row += 1

    ws_r.freeze_panes = "A3"
```

**Step 2: Verificare sintassi**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import ast, sys
with open('ComparativoPreventivi.py') as f:
    src = f.read()
try:
    ast.parse(src)
    print('OK: sintassi corretta')
except SyntaxError as e:
    print(f'ERRORE: {e}')
    sys.exit(1)
"
```

---

## Task 6: Riscrivere la GUI con design moderno

**Files:**
- Modify: `ComparativoPreventivi.py` (classe `App`, righe 447-695)

**Obiettivo design:**
- Finestra ridimensionabile (min 780×700)
- Palette: sfondo `#0F1117` (quasi nero), card `#1E2130`, accent `#4F8EF7` (blu elettrico), testo `#E8EAF0`
- Liste preventivi dinamiche: ogni file ha riga con icona, nome file, campo testo editabile per il nome azienda, bottone rimozione
- Animazione semplice: pulsante GENERA cambia colore durante elaborazione
- Scrollable area per la lista preventivi (può essere lunga)

**Step 1: Sostituire l'intera classe App e il blocco `if __name__ == "__main__":`**

```python
# ─────────────────────────────────────────────────────────────────
#  INTERFACCIA GRAFICA — Design Moderno
# ─────────────────────────────────────────────────────────────────

class PreventivRow(tk.Frame):
    """
    Widget riga per un singolo preventivo nella lista.
    Mostra: icona file | nome file (troncato) | campo nome azienda | bottone rimuovi
    """
    BG = "#1E2130"
    BG_HOVER = "#262B3D"
    ACCENT = "#4F8EF7"
    TEXT = "#E8EAF0"
    TEXT_MUTED = "#8892A4"
    RED = "#E05C5C"

    def __init__(self, parent, filepath, on_remove, index, **kwargs):
        super().__init__(parent, bg=self.BG, **kwargs)
        self.filepath = filepath
        self.on_remove = on_remove
        self.index = index

        self.columnconfigure(1, weight=1)

        # Icona/indice
        lbl_idx = tk.Label(self, text=f"  {index+1:02d}", bg=self.BG,
                           fg=self.ACCENT, font=("Consolas", 9, "bold"), width=4)
        lbl_idx.grid(row=0, column=0, padx=(8, 4), pady=6)

        # Nome file (troncato a 30 char)
        nome_file = os.path.basename(filepath)
        nome_troncato = (nome_file[:28] + "…") if len(nome_file) > 30 else nome_file
        lbl_file = tk.Label(self, text=nome_troncato, bg=self.BG,
                            fg=self.TEXT_MUTED, font=("Consolas", 8), anchor="w", width=30)
        lbl_file.grid(row=0, column=1, padx=4, pady=6, sticky="w")

        # Campo nome azienda
        self.nome_var = tk.StringVar(value=os.path.splitext(nome_file)[0].upper())
        entry = tk.Entry(self, textvariable=self.nome_var,
                         bg="#2A3044", fg=self.TEXT,
                         insertbackground=self.ACCENT,
                         relief="flat", font=("Arial", 9), width=24,
                         highlightthickness=1, highlightbackground="#3A4258",
                         highlightcolor=self.ACCENT)
        entry.grid(row=0, column=2, padx=(4, 8), pady=6, ipady=3)

        # Bottone rimuovi
        btn_rm = tk.Button(self, text="✕", bg=self.BG, fg="#5A6070",
                           activebackground=self.RED, activeforeground="white",
                           font=("Arial", 9, "bold"), relief="flat",
                           cursor="hand2", width=2,
                           command=lambda: self.on_remove(self.filepath))
        btn_rm.grid(row=0, column=3, padx=(0, 8))

        # Separatore sotto
        sep = tk.Frame(self, bg="#262B3D", height=1)
        sep.grid(row=1, column=0, columnspan=4, sticky="ew")

    def get_nome_azienda(self):
        n = self.nome_var.get().strip()
        return n if n else os.path.splitext(os.path.basename(self.filepath))[0].upper()


class ScrollableFrame(tk.Frame):
    """Frame con scrollbar verticale interna."""
    def __init__(self, parent, bg, **kwargs):
        super().__init__(parent, bg=bg, **kwargs)
        self.canvas = tk.Canvas(self, bg=bg, highlightthickness=0)
        self.scrollbar = tk.Scrollbar(self, orient="vertical",
                                       command=self.canvas.yview)
        self.inner = tk.Frame(self.canvas, bg=bg)
        self.inner_id = self.canvas.create_window((0, 0), window=self.inner,
                                                   anchor="nw")
        self.inner.bind("<Configure>", self._on_inner_configure)
        self.canvas.bind("<Configure>", self._on_canvas_configure)
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
        self.canvas.pack(side="left", fill="both", expand=True)
        self.scrollbar.pack(side="right", fill="y")

    def _on_inner_configure(self, _event):
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))
        h = self.inner.winfo_reqheight()
        self.canvas.configure(height=min(h, 200))

    def _on_canvas_configure(self, event):
        self.canvas.itemconfig(self.inner_id, width=event.width)


class App(tk.Tk):
    # Palette dark moderna
    BG_ROOT    = "#0F1117"
    BG_CARD    = "#1E2130"
    BG_CARD2   = "#161924"
    ACCENT     = "#4F8EF7"
    ACCENT_HOV = "#3A7AE8"
    TEXT       = "#E8EAF0"
    TEXT_MUTED = "#8892A4"
    TEXT_DIM   = "#5A6070"
    GREEN      = "#3DBE7A"
    GREEN_HOV  = "#2EA86A"
    RED        = "#E05C5C"
    ORANGE     = "#E8903A"
    BORDER     = "#262B3D"

    def __init__(self):
        super().__init__()
        self.title("Comparativo Preventivi — Studio di Architettura")
        self.configure(bg=self.BG_ROOT)
        self.minsize(820, 720)

        w, h = 860, 760
        sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
        self.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")

        self.capitolato_path = StringVar(value="")
        self.preventivi_data = []   # lista di {"path": str, "widget": PreventivRow}
        self.output_dir = StringVar(value=os.path.expanduser("~/Desktop"))
        self.soglia_var = tk.IntVar(value=SOGLIA_ANOMALIA_PERC)

        self._build_ui()
        self._check_openpyxl()

    def _check_openpyxl(self):
        if not OPENPYXL_OK:
            self._log("ERRORE: libreria 'openpyxl' non installata.", level="err")
            self._log("  Apri il Prompt dei comandi e digita: pip install openpyxl", level="err")
            self.btn_genera.config(state=DISABLED)

    def _card(self, parent, pady=(0, 12)):
        """Crea un frame 'card' con sfondo BG_CARD e padding."""
        outer = tk.Frame(parent, bg=self.BORDER, pady=1)
        outer.pack(fill="x", pady=pady)
        inner = tk.Frame(outer, bg=self.BG_CARD)
        inner.pack(fill="x", padx=1, pady=1)
        return inner

    def _label_section(self, parent, text):
        tk.Label(parent, text=text.upper(), bg=self.BG_ROOT,
                 fg=self.ACCENT, font=("Arial", 8, "bold"),
                 anchor="w").pack(fill="x", pady=(14, 4))

    def _build_ui(self):
        # ── Header ──
        hdr = tk.Frame(self, bg=self.BG_CARD2)
        hdr.pack(fill="x")
        tk.Frame(hdr, bg=self.ACCENT, height=3).pack(fill="x")
        hdr_inner = tk.Frame(hdr, bg=self.BG_CARD2)
        hdr_inner.pack(fill="x", padx=28, pady=14)
        tk.Label(hdr_inner, text="COMPARATIVO PREVENTIVI",
                 bg=self.BG_CARD2, fg=self.TEXT,
                 font=("Arial", 16, "bold")).pack(side="left")
        tk.Label(hdr_inner, text="  —  Studio di Architettura",
                 bg=self.BG_CARD2, fg=self.TEXT_MUTED,
                 font=("Arial", 11)).pack(side="left", pady=4)

        # ── Body scrollabile ──
        body = tk.Frame(self, bg=self.BG_ROOT)
        body.pack(fill="both", expand=True, padx=28, pady=8)

        # Step 1: Capitolato
        self._label_section(body, "1  —  Capitolato di riferimento")
        card1 = self._card(body)
        card1_inner = tk.Frame(card1, bg=self.BG_CARD)
        card1_inner.pack(fill="x", padx=12, pady=10)
        self.lbl_cap = tk.Label(card1_inner, textvariable=self.capitolato_path,
                                bg="#12151F", fg=self.TEXT_MUTED,
                                font=("Consolas", 9), anchor="w",
                                relief="flat", padx=10)
        self.lbl_cap.pack(side="left", fill="x", expand=True, ipady=7)
        tk.Button(card1_inner, text="Sfoglia…", command=self._scegli_capitolato,
                  bg=self.ACCENT, fg="white", font=("Arial", 9, "bold"),
                  relief="flat", cursor="hand2", padx=14,
                  activebackground=self.ACCENT_HOV,
                  activeforeground="white").pack(side="left", padx=(8, 0), ipady=5)

        # Step 2: Preventivi
        self._label_section(body, "2  —  Preventivi imprese")
        card2 = self._card(body)
        # Toolbar
        toolbar = tk.Frame(card2, bg=self.BG_CARD)
        toolbar.pack(fill="x", padx=12, pady=(10, 6))
        tk.Button(toolbar, text="＋  Aggiungi file…",
                  command=self._aggiungi_preventivi,
                  bg=self.ACCENT, fg="white", font=("Arial", 9, "bold"),
                  relief="flat", cursor="hand2", padx=12,
                  activebackground=self.ACCENT_HOV,
                  activeforeground="white").pack(side="left", ipady=5)
        tk.Button(toolbar, text="Rimuovi tutti",
                  command=self._rimuovi_tutti,
                  bg=self.BG_CARD, fg=self.TEXT_DIM, font=("Arial", 9),
                  relief="flat", cursor="hand2", padx=10,
                  activebackground=self.BORDER).pack(side="left", padx=8, ipady=5)
        tk.Label(toolbar, text="Inserisci il nome azienda per ogni file →",
                 bg=self.BG_CARD, fg=self.TEXT_DIM,
                 font=("Arial", 8, "italic")).pack(side="right")

        # Lista preventivi (scrollable)
        self.scroll_frame = ScrollableFrame(card2, bg=self.BG_CARD)
        self.scroll_frame.pack(fill="x", padx=12, pady=(0, 10))
        self.lbl_vuoto = tk.Label(self.scroll_frame.inner,
                                  text="  Nessun file caricato",
                                  bg=self.BG_CARD, fg=self.TEXT_DIM,
                                  font=("Arial", 9), anchor="w")
        self.lbl_vuoto.pack(fill="x", pady=10)

        # Step 3: Output
        self._label_section(body, "3  —  Cartella di destinazione")
        card3 = self._card(body)
        card3_inner = tk.Frame(card3, bg=self.BG_CARD)
        card3_inner.pack(fill="x", padx=12, pady=10)
        self.lbl_out = tk.Label(card3_inner, textvariable=self.output_dir,
                                bg="#12151F", fg=self.TEXT_MUTED,
                                font=("Consolas", 9), anchor="w",
                                relief="flat", padx=10)
        self.lbl_out.pack(side="left", fill="x", expand=True, ipady=7)
        tk.Button(card3_inner, text="Cambia…", command=self._scegli_output,
                  bg=self.ACCENT, fg="white", font=("Arial", 9, "bold"),
                  relief="flat", cursor="hand2", padx=14,
                  activebackground=self.ACCENT_HOV,
                  activeforeground="white").pack(side="left", padx=(8, 0), ipady=5)

        # Soglia anomalie
        card4 = self._card(body, pady=(0, 16))
        soglia_inner = tk.Frame(card4, bg=self.BG_CARD)
        soglia_inner.pack(fill="x", padx=12, pady=10)
        tk.Label(soglia_inner, text="Soglia anomalie:",
                 bg=self.BG_CARD, fg=self.TEXT_MUTED,
                 font=("Arial", 9)).pack(side="left")
        soglia_spin = tk.Spinbox(soglia_inner, from_=5, to=50, increment=5,
                                  textvariable=self.soglia_var, width=4,
                                  font=("Arial", 10, "bold"),
                                  fg=self.ACCENT, bg="#12151F",
                                  buttonbackground=self.BG_CARD,
                                  relief="flat",
                                  insertbackground=self.ACCENT)
        soglia_spin.pack(side="left", padx=8)
        tk.Label(soglia_inner,
                 text="% — prezzi oltre questa soglia dalla media vengono evidenziati",
                 bg=self.BG_CARD, fg=self.TEXT_MUTED,
                 font=("Arial", 9)).pack(side="left")

        # ── Pulsante GENERA ──
        btn_frame = tk.Frame(self, bg=self.BG_ROOT)
        btn_frame.pack(fill="x", padx=28, pady=(4, 8))
        self.btn_genera = tk.Button(btn_frame,
                                     text="⚡  GENERA COMPARATIVO",
                                     command=self._genera,
                                     bg=self.GREEN, fg="white",
                                     font=("Arial", 13, "bold"),
                                     relief="flat", cursor="hand2",
                                     activebackground=self.GREEN_HOV,
                                     activeforeground="white")
        self.btn_genera.pack(fill="x", ipady=12)

        # ── Log ──
        log_frame = tk.Frame(self, bg=self.BG_ROOT)
        log_frame.pack(fill="both", expand=True, padx=28, pady=(0, 16))
        tk.Label(log_frame, text="LOG", bg=self.BG_ROOT,
                 fg=self.ACCENT, font=("Arial", 8, "bold"),
                 anchor="w").pack(fill="x", pady=(0, 4))
        log_card = tk.Frame(log_frame, bg=self.BORDER, pady=1)
        log_card.pack(fill="both", expand=True)
        log_inner = tk.Frame(log_card, bg=self.BORDER)
        log_inner.pack(fill="both", expand=True, padx=1, pady=1)
        self.log_text = Text(log_inner, height=7,
                              font=("Consolas", 9),
                              bg="#080C14", fg="#C8D0E0",
                              relief="flat", state=DISABLED,
                              wrap=WORD, padx=12, pady=8,
                              insertbackground=self.ACCENT)
        sb = Scrollbar(log_inner, command=self.log_text.yview,
                       bg=self.BG_CARD, troughcolor=self.BG_ROOT)
        self.log_text.configure(yscrollcommand=sb.set)
        self.log_text.pack(side="left", fill="both", expand=True)
        sb.pack(side="right", fill="y")
        self.log_text.tag_config("ok",   foreground="#3DBE7A")
        self.log_text.tag_config("err",  foreground="#E05C5C")
        self.log_text.tag_config("info", foreground="#7EB8F7")
        self.log_text.tag_config("warn", foreground="#E8903A")

        self._log("Pronto. Carica il capitolato e i preventivi delle imprese.", level="info")

    def _scegli_capitolato(self):
        path = filedialog.askopenfilename(
            title="Seleziona il capitolato di riferimento",
            filetypes=[("Excel", "*.xlsx *.xls"), ("Tutti i file", "*.*")])
        if path:
            self.capitolato_path.set(path)
            self._log(f"Capitolato: {os.path.basename(path)}", level="info")

    def _aggiungi_preventivi(self):
        paths = filedialog.askopenfilenames(
            title="Seleziona uno o più preventivi",
            filetypes=[("Excel", "*.xlsx *.xls"), ("Tutti i file", "*.*")])
        esistenti = {d["path"] for d in self.preventivi_data}
        for p in paths:
            if p not in esistenti:
                self._aggiungi_riga(p)
        self._aggiorna_vuoto()

    def _aggiungi_riga(self, filepath):
        idx = len(self.preventivi_data)
        row_widget = PreventivRow(self.scroll_frame.inner, filepath,
                                   on_remove=self._rimuovi_path,
                                   index=idx)
        row_widget.pack(fill="x")
        self.preventivi_data.append({"path": filepath, "widget": row_widget})

    def _rimuovi_path(self, filepath):
        self.preventivi_data = [d for d in self.preventivi_data
                                 if d["path"] != filepath]
        # Ricrea la lista da zero (semplice, N è sempre piccolo)
        for widget in self.scroll_frame.inner.winfo_children():
            widget.destroy()
        self.lbl_vuoto = tk.Label(self.scroll_frame.inner,
                                   text="  Nessun file caricato",
                                   bg=self.BG_CARD, fg=self.TEXT_DIM,
                                   font=("Arial", 9), anchor="w")
        self.lbl_vuoto.pack(fill="x", pady=10)
        tmp = list(self.preventivi_data)
        self.preventivi_data = []
        for d in tmp:
            self._aggiungi_riga(d["path"])
        self._aggiorna_vuoto()

    def _rimuovi_tutti(self):
        self.preventivi_data = []
        for widget in self.scroll_frame.inner.winfo_children():
            widget.destroy()
        self.lbl_vuoto = tk.Label(self.scroll_frame.inner,
                                   text="  Nessun file caricato",
                                   bg=self.BG_CARD, fg=self.TEXT_DIM,
                                   font=("Arial", 9), anchor="w")
        self.lbl_vuoto.pack(fill="x", pady=10)
        self._aggiorna_vuoto()

    def _aggiorna_vuoto(self):
        if self.preventivi_data:
            self.lbl_vuoto.pack_forget()
        else:
            self.lbl_vuoto.pack(fill="x", pady=10)

    def _scegli_output(self):
        d = filedialog.askdirectory(title="Scegli cartella di destinazione")
        if d:
            self.output_dir.set(d)

    def _log(self, msg, level="info"):
        self.log_text.config(state=NORMAL)
        self.log_text.insert(END, msg + "\n", level)
        self.log_text.see(END)
        self.log_text.config(state=DISABLED)
        self.update_idletasks()

    def _genera(self):
        global SOGLIA_ANOMALIA_PERC
        cap = self.capitolato_path.get()
        if not cap or not os.path.exists(cap):
            messagebox.showerror("Errore", "Seleziona prima il file capitolato.")
            return
        if not self.preventivi_data:
            messagebox.showerror("Errore", "Aggiungi almeno un preventivo.")
            return

        SOGLIA_ANOMALIA_PERC = self.soglia_var.get()

        paths = [d["path"] for d in self.preventivi_data]
        nomi  = [d["widget"].get_nome_azienda() for d in self.preventivi_data]

        data_str = datetime.now().strftime("%Y%m%d_%H%M")
        output_path = os.path.join(self.output_dir.get(),
                                   f"comparativo_{data_str}.xlsx")

        self.btn_genera.config(state=DISABLED, text="⏳  Elaborazione in corso…",
                                bg=self.ORANGE)
        self._log("─" * 55, level="info")
        self._log(f"Avvio — soglia {SOGLIA_ANOMALIA_PERC}%  |  "
                  f"{len(paths)} imprese: {', '.join(nomi)}", level="info")

        def task():
            try:
                genera_comparativo(cap, paths, nomi, output_path,
                                   log_fn=self._log)
                self.after(0, lambda: self._genera_ok(output_path))
            except Exception as e:
                import traceback
                self.after(0, lambda: self._genera_err(
                    str(e) + "\n\n" + traceback.format_exc()))

        threading.Thread(target=task, daemon=True).start()

    def _genera_ok(self, output_path):
        self.btn_genera.config(state=NORMAL,
                                text="⚡  GENERA COMPARATIVO",
                                bg=self.GREEN)
        self._log(f"Completato: {output_path}", level="ok")
        if messagebox.askyesno("Completato",
                                f"Comparativo generato con successo!\n\n"
                                f"{output_path}\n\nAprire il file ora?"):
            try:
                os.startfile(output_path)
            except AttributeError:
                import subprocess
                subprocess.run(["open", output_path])

    def _genera_err(self, err):
        self.btn_genera.config(state=NORMAL,
                                text="⚡  GENERA COMPARATIVO",
                                bg=self.GREEN)
        self._log(f"ERRORE: {err}", level="err")
        messagebox.showerror("Errore", f"Si è verificato un errore:\n\n{err[:400]}")


if __name__ == "__main__":
    if not OPENPYXL_OK:
        try:
            root = Tk()
            root.withdraw()
            messagebox.showerror(
                "Libreria mancante",
                "La libreria 'openpyxl' non è installata.\n\n"
                "Apri il Prompt dei comandi e digita:\n\n"
                "    pip install openpyxl\n\n"
                "Poi riavvia l'applicazione.")
            root.destroy()
        except Exception:
            print("ERRORE: pip install openpyxl")
        sys.exit(1)

    app = App()
    app.mainloop()
```

**Step 2: Verificare sintassi**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import ast, sys
with open('ComparativoPreventivi.py') as f:
    src = f.read()
try:
    ast.parse(src)
    print('OK: sintassi corretta')
except SyntaxError as e:
    print(f'ERRORE: {e}')
    sys.exit(1)
"
```

---

## Task 7: Test funzionale end-to-end

**Files:**
- Read: `FILES/capitolato_BASE.xlsx`
- Read: `FILES/impresa 1.xlsx`

**Step 1: Avviare l'applicazione e verificare che si apra senza errori**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import sys
# Test import di tutti i moduli necessari
try:
    from ComparativoPreventivi import (
        estrai_firma_intestazione,
        trova_riga_intestazione,
        leggi_preventivo_v2,
        leggi_capitolato,
        genera_comparativo
    )
    print('OK: tutti i simboli importabili')
except ImportError as e:
    print(f'ERRORE import: {e}')
    sys.exit(1)
except Exception as e:
    print(f'ERRORE: {e}')
    sys.exit(1)
"
```

Risultato atteso: `OK: tutti i simboli importabili`

**Step 2: Test lettura capitolato**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
from ComparativoPreventivi import leggi_capitolato, estrai_firma_intestazione
struttura = leggi_capitolato('FILES/capitolato_BASE.xlsx')
voci = [s for s in struttura if s['tipo'] == 'voce']
sezioni = [s for s in struttura if s['tipo'] == 'sezione']
print(f'Capitolato: {len(voci)} voci, {len(sezioni)} sezioni')
firma = estrai_firma_intestazione('FILES/capitolato_BASE.xlsx')
print(f'Firma intestazione: {firma}')
"
```

Risultato atteso: output con numero voci > 0 e firma con almeno 2-3 token non vuoti.

**Step 3: Test lettura preventivo**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
from ComparativoPreventivi import (leggi_capitolato, estrai_firma_intestazione,
                                    leggi_preventivo_v2)
struttura = leggi_capitolato('FILES/capitolato_BASE.xlsx')
numeri = {v['num'] for v in struttura if v['tipo'] == 'voce'}
firma = estrai_firma_intestazione('FILES/capitolato_BASE.xlsx')
voci, note = leggi_preventivo_v2('FILES/impresa 1.xlsx', firma, numeri)
prezzi = {k: v for k, v in voci.items() if v is not None and v > 0}
print(f'Voci prezzate: {len(prezzi)} / {len(numeri)}')
print(f'Note extra: {len(note)}')
if prezzi:
    primo = next(iter(prezzi.items()))
    print(f'Esempio: voce {primo[0]} = {primo[1]}')
"
```

Risultato atteso: almeno alcune voci prezzate, nessun errore.

**Step 4: Test generazione comparativo completo**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
python -c "
import os
from ComparativoPreventivi import genera_comparativo
output = '/tmp/test_comparativo.xlsx'
genera_comparativo(
    'FILES/capitolato_BASE.xlsx',
    ['FILES/impresa 1.xlsx', 'FILES/impresa .xlsx'],
    ['Impresa Alpha', 'Impresa Beta'],
    output,
    log_fn=print
)
size = os.path.getsize(output)
print(f'File generato: {size} bytes')
assert size > 5000, 'File troppo piccolo!'
print('OK: test superato')
"
```

Risultato atteso: file generato con size > 5000 bytes, nessun errore.

---

## Task 8: Aggiornare `MEMORIA_PROGETTO_ComparativoPreventivi.md`

**Files:**
- Modify: `MEMORIA_PROGETTO_ComparativoPreventivi.md`

**Step 1: Aggiungere sezione v2 alla memoria del progetto**

Aggiungere in cima al file (dopo il titolo) una sezione che documenta le modifiche v2:

```markdown
## Versione 2 — Modifiche (01/03/2026)

| Criticità | Soluzione implementata |
|---|---|
| 1. Fedeltà capitolato | Garantita: tutte le righe del capitolato sono presenti nel comparativo |
| 2. Righe extra in testa | `trova_riga_intestazione()` + `estrai_firma_intestazione()`: confronto automatico con firma capitolato |
| 3. Nome azienda dalla GUI | `PreventivRow` widget con campo Entry editabile per ogni file caricato |
| 4. Layout colonne | Prima tutti i P.U., poi tutti i Totali: `[PU_1..PU_N][MEDIA][TOT_1..TOT_N][NOTE]` |
| 5. Note extra intermedie | `leggi_preventivo_v2()` rileva righe con num voce non in capitolato → `note_extra` → righe grigie nel comparativo |

### Nuove funzioni v2
- `estrai_firma_intestazione(capitolato_path)` → lista token intestazione (riga 2, colonne A-F)
- `trova_riga_intestazione(ws, firma, soglia=2)` → num riga intestazione nel file impresa
- `leggi_preventivo_v2(filepath, firma, numeri_voce)` → (voci_dict, note_extra_list)
- `genera_comparativo(...)` ora accetta `nomi_imprese: list[str]` come parametro esplicito

### Nuovi widget GUI
- `PreventivRow`: riga per ogni preventivo con campo nome azienda editabile
- `ScrollableFrame`: lista preventivi scrollabile
```

---

## Riepilogo esecuzione

**Ordine task:** 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8

**Verifica finale:** Dopo Task 7, aprire il file `/tmp/test_comparativo.xlsx` in Excel/LibreOffice e verificare visivamente:
- Layout colonne corretto (tutti P.U. prima, poi tutti Totali)
- Nomi imprese "Impresa Alpha" e "Impresa Beta" nelle intestazioni
- Tutte le voci del capitolato presenti
- Nessuna riga mancante
