# SheetJS Client-Side Reading Implementation Plan

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

**Goal:** Eliminare completamente PhpSpreadsheet dalla pipeline di generazione — il browser scarica i file Excel e li legge con SheetJS, poi ExcelJS genera il comparativo, senza che PHP tocchi mai i file Excel.

**Architecture:**
- `api/servi_file.php` — nuovo endpoint che serve i file .xlsx raw (autenticato, con header Content-Type corretto)
- `api/genera.php` — ridotto a soli metadati: lista file con ID/nome impresa/soglia, **senza aprire nessun Excel**
- `assets/js/sheetjs-reader.js` — nuovo: legge capitolato e preventivi con SheetJS in browser, produce `{struttura, imprese, medie}`
- `assets/js/comparativo.js` — aggiorna `generaComparativo()` per usare sheetjs-reader invece di chiamare genera.php per i dati Excel
- `pages/comparativo.php` — aggiunge SheetJS CDN

**Tech Stack:** PHP (solo metadati DB) · SheetJS 0.20.x (MIT, CDN) · ExcelJS 4.4.x (già presente) · JS vanilla

---

## Costanti e logica di lettura (da replicare in SheetJS)

Regola universale dal CLAUDE.md:
> **Col D numerico = riga dati** (quantità presente → prezzo atteso nelle colonne successive).

Per il capitolato: colonne A, B, C, D (indici 0, 1, 2, 3 in SheetJS)
Per i preventivi: colonne A, D, E (indici 0, 3, 4 in SheetJS)

Riga intestazione: prima riga con col A === '#' (altrimenti riga 1)

---

## Task 1: Crea `api/servi_file.php` — serve file xlsx autenticato

**Files:**
- Create: `comparativo/api/servi_file.php`

**Step 1: Scrivere il file**

```php
<?php
// servi_file.php — serve un file xlsx caricato (uploads) come download binario
// Parametri GET: ?file_id=<id>
require_once __DIR__ . '/../core/auth.php';
richiedeLogin();

$fileId = intval($_GET['file_id'] ?? 0);
if (!$fileId) {
    http_response_code(400);
    exit('Parametro file_id mancante');
}

$db = getDB();
$stmt = $db->prepare('SELECT * FROM file_comparativo WHERE id = ?');
$stmt->execute([$fileId]);
$file = $stmt->fetch();

if (!$file) {
    http_response_code(404);
    exit('File non trovato');
}

if (!file_exists($file['percorso'])) {
    http_response_code(404);
    exit('File fisico non trovato');
}

$ext = strtolower(pathinfo($file['percorso'], PATHINFO_EXTENSION));
$mime = ($ext === 'xlsx')
    ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    : 'application/vnd.ms-excel';

header('Content-Type: ' . $mime);
header('Content-Length: ' . filesize($file['percorso']));
header('Cache-Control: private, max-age=300');
readfile($file['percorso']);
exit;
```

**Step 2: Verifica manuale**

Aprire in browser: `http://<NAS>/comparativo/api/servi_file.php?file_id=1`
Deve restituire il file binario .xlsx (o 404 se non esiste). NON deve aprire una pagina HTML.

---

## Task 2: Semplifica `api/genera.php` — solo metadati, zero Excel

**Files:**
- Modify: `comparativo/api/genera.php`

**Step 1: Sostituire tutto il contenuto**

Il nuovo genera.php NON carica nessun file Excel. Legge solo il DB e restituisce:
- ID e percorso di ogni file (capitolato + preventivi)
- Nome impresa per ogni preventivo
- Soglia anomalie del comparativo

```php
<?php
// genera.php — restituisce metadati per la generazione client-side
// NON legge file Excel. PHP non tocca mai i file xlsx.
require_once __DIR__ . '/../core/auth.php';
richiedeLogin();

header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['ok' => false, 'errore' => 'Metodo non consentito']);
    exit;
}

$input = json_decode(file_get_contents('php://input'), true);
$comparativoId = intval($input['comparativo_id'] ?? 0);
if (!$comparativoId) {
    echo json_encode(['ok' => false, 'errore' => 'comparativo_id mancante']);
    exit;
}

$db = getDB();

$stmt = $db->prepare('SELECT * FROM comparativi WHERE id = ?');
$stmt->execute([$comparativoId]);
$comp = $stmt->fetch();
if (!$comp) {
    echo json_encode(['ok' => false, 'errore' => 'Comparativo non trovato']);
    exit;
}

$stmt = $db->prepare(
    'SELECT id, tipo, nome_impresa, nome_file
     FROM file_comparativo
     WHERE comparativo_id = ?
     ORDER BY tipo DESC, id ASC'
);
$stmt->execute([$comparativoId]);
$files = $stmt->fetchAll();

$capitolato = null;
$preventivi = [];
foreach ($files as $f) {
    if ($f['tipo'] === 'capitolato') {
        $capitolato = ['file_id' => intval($f['id']), 'nome_file' => $f['nome_file']];
    } else {
        $preventivi[] = [
            'file_id'     => intval($f['id']),
            'nome_impresa' => $f['nome_impresa'] ?: $f['nome_file'],
            'nome_file'   => $f['nome_file'],
        ];
    }
}

if (!$capitolato) {
    echo json_encode(['ok' => false, 'errore' => 'Capitolato non caricato']);
    exit;
}
if (empty($preventivi)) {
    echo json_encode(['ok' => false, 'errore' => 'Nessun preventivo caricato']);
    exit;
}

echo json_encode([
    'ok'         => true,
    'soglia'     => intval($comp['soglia'] ?? 20),
    'capitolato' => $capitolato,
    'preventivi' => $preventivi,
]);
```

**Step 2: Verifica**

Aprire DevTools → Network → premere GENERA → verificare che la risposta JSON sia piccola (solo metadati, nessun dato Excel) e arrivi in meno di 1 secondo.

---

## Task 3: Crea `assets/js/sheetjs-reader.js` — lettura Excel in browser

**Files:**
- Create: `comparativo/assets/js/sheetjs-reader.js`

Questo file replica la logica di `core/lettore.php` in JavaScript usando SheetJS.

**Step 1: Scrivere il file completo**

```javascript
// sheetjs-reader.js — lettura file Excel in browser con SheetJS
// Replica la logica di core/lettore.php
// Richiede: SheetJS (XLSX) caricato via CDN prima di questo script

/**
 * Scarica un file xlsx dal server e lo legge con SheetJS.
 * @param {number} fileId - ID del file in file_comparativo
 * @returns {Promise<XLSX.WorkBook>}
 */
async function _scaricaExcel(fileId) {
    const resp = await fetch(`../api/servi_file.php?file_id=${fileId}`);
    if (!resp.ok) throw new Error(`Errore scaricamento file ${fileId}: HTTP ${resp.status}`);
    const arrayBuffer = await resp.arrayBuffer();
    return XLSX.read(arrayBuffer, { type: 'array', cellDates: false, sheetStubs: false });
}

/**
 * Trova la riga con '#' in colonna A (intestazione).
 * @param {Array<Array>} rows - array di righe (output di sheet_to_json con header:1)
 * @param {number} maxRiga - quante righe controllare
 * @returns {number} indice 0-based della riga '#', oppure -1
 */
function _trovaRigaHash(rows, maxRiga = 200) {
    const limit = Math.min(rows.length, maxRiga);
    for (let i = 0; i < limit; i++) {
        const valA = rows[i]?.[0];
        if (valA !== undefined && valA !== null && String(valA).trim() === '#') {
            return i;
        }
    }
    return -1;
}

/**
 * Estrae la "firma" del capitolato: valori normalizzati della riga '#'.
 * @param {Array<Array>} rows
 * @param {number} rigaHashIdx - indice 0-based della riga '#'
 * @returns {string[]} - array di token lowercase, max 6 colonne
 */
function _estraiFirma(rows, rigaHashIdx) {
    if (rigaHashIdx < 0) return [];
    const riga = rows[rigaHashIdx] || [];
    const firma = [];
    for (let c = 0; c < 6; c++) {
        const v = riga[c];
        firma.push(v !== undefined && v !== null ? String(v).toLowerCase().trim() : '');
    }
    return firma;
}

/**
 * Trova la riga intestazione nel preventivo.
 * Prima cerca '#' in col A; fallback: match testuale con firma capitolato.
 * @param {Array<Array>} rows
 * @param {string[]} firmaCapitolato
 * @returns {number} indice 0-based, oppure -1
 */
function _trovaRigaIntestazione(rows, firmaCapitolato, sogliaMatch = 2) {
    const maxRiga = Math.min(rows.length, 200);

    // Prima cerca '#'
    for (let i = 0; i < maxRiga; i++) {
        const valA = rows[i]?.[0];
        if (valA !== undefined && valA !== null && String(valA).trim() === '#') {
            return i;
        }
    }

    // Fallback: match testuale
    if (!firmaCapitolato || firmaCapitolato.length === 0) return -1;
    for (let i = 0; i < maxRiga; i++) {
        const riga = rows[i] || [];
        let match = 0;
        for (let c = 0; c < Math.min(6, firmaCapitolato.length); c++) {
            const token = firmaCapitolato[c];
            if (!token) continue;
            const valCella = riga[c] !== undefined && riga[c] !== null
                ? String(riga[c]).toLowerCase().trim()
                : '';
            if (valCella.includes(token)) match++;
        }
        if (match >= sogliaMatch) return i;
    }

    return -1;
}

/**
 * Legge il capitolato da un WorkBook SheetJS.
 * Regola: col D (indice 3) numerico = riga dati.
 * @returns {{ struttura: Array, firma: string[] }}
 */
function _leggiCapitolato(wb) {
    const ws = wb.Sheets[wb.SheetNames[0]];
    // header:1 → array di array (riga per riga)
    const rows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: null, blankrows: true });

    const rigaHashIdx = _trovaRigaHash(rows);
    const firma = _estraiFirma(rows, rigaHashIdx);

    const struttura = [];
    let contatoreVoce = 0;
    let vuoteConsecutive = 0;
    const MAX_VUOTE = 100;

    const inizio = rigaHashIdx >= 0 ? rigaHashIdx + 1 : 0;

    for (let i = inizio; i < rows.length; i++) {
        const riga = rows[i] || [];
        const colA = riga[0];
        const colB = riga[1];
        const colC = riga[2];
        const colD = riga[3];

        // Riga vuota
        const tuttoNull = (colA === null || colA === undefined || colA === '') &&
                          (colB === null || colB === undefined || colB === '') &&
                          (colC === null || colC === undefined || colC === '') &&
                          (colD === null || colD === undefined || colD === '');
        if (tuttoNull) {
            vuoteConsecutive++;
            if (vuoteConsecutive >= MAX_VUOTE) {
                // Rimuovi le vuote in eccesso già aggiunte
                struttura.splice(struttura.length - (MAX_VUOTE - 1));
                break;
            }
            struttura.push({ tipo: 'vuota' });
            continue;
        }
        vuoteConsecutive = 0;

        // Col D numerico → riga dati
        const numD = typeof colD === 'number' ? colD : parseFloat(colD);
        if (!isNaN(numD) && colD !== null && colD !== '') {
            contatoreVoce++;
            const numVoce = (colA !== null && colA !== undefined && String(colA).trim() !== '')
                ? String(colA).trim()
                : `__v${contatoreVoce}`;
            struttura.push({
                tipo: 'dati',
                col_a: colA !== null && colA !== undefined ? String(colA).trim() : null,
                col_b: colB !== null && colB !== undefined ? String(colB).trim() : null,
                col_c: colC !== null && colC !== undefined ? String(colC).trim() : null,
                col_d: numD,
                num_voce: numVoce,
            });
        } else {
            struttura.push({
                tipo: 'testo',
                col_a: colA !== null && colA !== undefined ? String(colA).trim() : null,
                col_b: colB !== null && colB !== undefined ? String(colB).trim() : null,
            });
        }
    }

    return { struttura, firma };
}

/**
 * Legge un preventivo da un WorkBook SheetJS.
 * Colonne: A=num_voce (indice 0), D=quantità (indice 3), E=prezzo (indice 4).
 * @param {object} wb - WorkBook SheetJS
 * @param {string[]} firma - firma del capitolato per trovare intestazione
 * @param {object} numeriVoce - set {num_voce: true} dal capitolato
 * @returns {object} - {num_voce: prezzo|null}
 */
function _leggiPreventivo(wb, firma, numeriVoce) {
    const ws = wb.Sheets[wb.SheetNames[0]];
    const rows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: null, blankrows: true });

    const rigaIntIdx = _trovaRigaIntestazione(rows, firma);
    const inizio = rigaIntIdx >= 0 ? rigaIntIdx + 1 : 0;

    const voci = {};
    let contatore = 0;
    let vuoteConsecutive = 0;
    const MAX_VUOTE = 100;

    for (let i = inizio; i < rows.length; i++) {
        const riga = rows[i] || [];
        const colA = riga[0];
        const colD = riga[3];
        const colE = riga[4];

        // Early exit su troppe righe vuote consecutive
        const tuttoNull = (colA === null || colA === undefined || colA === '') &&
                          (colD === null || colD === undefined || colD === '') &&
                          (colE === null || colE === undefined || colE === '');
        if (tuttoNull) {
            vuoteConsecutive++;
            if (vuoteConsecutive >= MAX_VUOTE) break;
            continue;
        }
        vuoteConsecutive = 0;

        // Solo righe con col D numerico
        const numD = typeof colD === 'number' ? colD : parseFloat(colD);
        if (isNaN(numD) || colD === null || colD === '') continue;

        contatore++;
        const numVoce = (colA !== null && colA !== undefined && String(colA).trim() !== '')
            ? String(colA).trim()
            : `__v${contatore}`;

        if (numeriVoce[numVoce] !== undefined) {
            const numE = typeof colE === 'number' ? colE : parseFloat(colE);
            voci[numVoce] = (!isNaN(numE) && colE !== null && colE !== '') ? numE : null;
        }
    }

    return voci;
}

/**
 * Entry point principale.
 * Scarica e legge capitolato + tutti i preventivi.
 * Calcola medie. Restituisce {struttura, imprese, medie, nomi, soglia}.
 *
 * @param {object} metadati - risposta da api/genera.php
 *   { soglia, capitolato: {file_id, nome_file}, preventivi: [{file_id, nome_impresa}] }
 * @param {function} onLog - callback(msg) per log in tempo reale
 * @returns {Promise<object>} - {struttura, imprese, medie, nomi, soglia}
 */
async function leggiTuttiFile(metadati, onLog = () => {}) {
    const { soglia, capitolato, preventivi } = metadati;

    // 1. Scarica e leggi capitolato
    onLog(`Scaricamento capitolato (${capitolato.nome_file})...`);
    const wbCap = await _scaricaExcel(capitolato.file_id);
    onLog('Lettura capitolato...');
    const { struttura, firma } = _leggiCapitolato(wbCap);

    // Costruisci set numeriVoce
    const numeriVoce = {};
    const righeDati = [];
    for (const r of struttura) {
        if (r.tipo === 'dati') {
            numeriVoce[r.num_voce] = true;
            righeDati.push(r);
        }
    }
    onLog(`Capitolato: ${righeDati.length} voci dati trovate.`);

    // 2. Scarica e leggi preventivi
    const imprese = [];
    const nomi = [];
    for (const prev of preventivi) {
        onLog(`Scaricamento preventivo: ${prev.nome_impresa} (${prev.nome_file})...`);
        const wb = await _scaricaExcel(prev.file_id);
        onLog(`Lettura preventivo: ${prev.nome_impresa}...`);
        const voci = _leggiPreventivo(wb, firma, numeriVoce);

        const vociPrezzate = Object.values(voci).filter(v => v !== null && v > 0).length;
        onLog(`${prev.nome_impresa}: ${vociPrezzate}/${righeDati.length} voci prezzate.`);

        imprese.push({ nome: prev.nome_impresa, voci });
        nomi.push(prev.nome_impresa);
    }

    // 3. Calcola medie (stesso algoritmo di PHP: prima occorrenza per num_voce, esclude null/0)
    onLog('Calcolo medie...');
    const medie = {};
    const vociGiaCalcolate = new Set();
    for (const r of righeDati) {
        const num = r.num_voce;
        if (vociGiaCalcolate.has(num)) continue;
        vociGiaCalcolate.add(num);
        const prezziValidi = imprese
            .map(imp => imp.voci[num] ?? null)
            .filter(p => p !== null && p > 0);
        medie[num] = prezziValidi.length > 0
            ? prezziValidi.reduce((a, b) => a + b, 0) / prezziValidi.length
            : null;
    }

    return { struttura, imprese, medie, nomi, soglia };
}
```

**Step 2: Non ci sono test automatici applicabili** (lettura file Excel binari in browser non si testa in modo semplice senza infrastruttura). La verifica è manuale via console.

---

## Task 4: Aggiorna `assets/js/comparativo.js` — usa sheetjs-reader

**Files:**
- Modify: `comparativo/assets/js/comparativo.js`

**Step 1: Sostituire la funzione `generaComparativo()`**

Trovare e sostituire SOLO la funzione `generaComparativo()` (da `async function generaComparativo()` fino alla chiusura `}`).

Il nuovo flusso:
1. POST `genera.php` → metadati (veloce, solo DB)
2. `leggiTuttiFile(metadati, onLog)` → SheetJS legge i file in browser
3. `generaExcel(dati, onLog)` → ExcelJS genera il file (già esistente)
4. `_costruisciPreview(...)` → preview HTML (già esistente)
5. POST `upload_output.php` → salva xlsx sul server

```javascript
async function generaComparativo() {
    const btn = document.getElementById('btn-genera');
    const testoOriginale = btn.innerHTML;
    btn.disabled = true;
    btn.innerHTML = '<span class="spinner-border spinner-sm" role="status"></span> Generazione in corso...';

    const logCard = document.getElementById('log-card');
    const logContainer = document.getElementById('log-container');
    logCard.style.display = '';
    logContainer.innerHTML = '';

    function appendLog(msg, cls = 'log-line') {
        const div = document.createElement('div');
        div.className = cls;
        div.textContent = msg;
        logContainer.appendChild(div);
        logContainer.scrollTop = logContainer.scrollHeight;
    }

    try {
        // 1. Recupera metadati dal server (solo DB, istantaneo)
        appendLog('Recupero metadati...');
        const metadati = await apiFetch('../api/genera.php', {
            method: 'POST',
            body: JSON.stringify({ comparativo_id: COMP_ID })
        });

        if (!metadati || !metadati.ok) {
            appendLog(metadati?.errore || 'Errore metadati', 'log-line log-errore');
            mostraAlert(metadati?.errore || 'Errore recupero metadati');
            return;
        }

        // 2. Scarica e leggi i file Excel in browser con SheetJS
        const dati = await leggiTuttiFile(metadati, appendLog);

        // 3. Genera Excel in browser con ExcelJS
        const blob = await generaExcel(dati, appendLog);

        // 4. Render preview
        appendLog('Rendering anteprima...');
        renderPreview(_costruisciPreview(dati.struttura, dati.imprese, dati.medie, dati.soglia), dati.nomi);

        // 5. Carica xlsx sul server per download futuro
        appendLog('Caricamento file sul server...');
        const formData = new FormData();
        formData.append('comparativo_id', COMP_ID);
        formData.append('file', blob, `comparativo_${Date.now()}.xlsx`);

        const resp = await fetch('../api/upload_output.php', { method: 'POST', body: formData });
        const uploadData = await resp.json();

        if (!uploadData || !uploadData.ok) {
            appendLog(uploadData?.errore || 'Errore upload', 'log-line log-errore');
            mostraAlert(uploadData?.errore || 'Errore caricamento sul server');
            return;
        }

        appendLog('Completato!', 'log-line log-ok');
        document.getElementById('btn-download').style.display = '';
        const badge = document.getElementById('badge-stato');
        badge.textContent = 'generato';
        badge.className = 'badge bg-success';

    } catch (e) {
        console.error(e);
        appendLog('Errore: ' + e.message, 'log-line log-errore');
        mostraAlert('Errore durante la generazione: ' + e.message);
    } finally {
        btn.disabled = false;
        btn.innerHTML = testoOriginale;
    }
}
```

**Step 2: Verificare che il resto del file rimanga invariato**

Le funzioni `uploadCapitolato`, `uploadPreventivi`, `aggiungiPreventivoLista`, `rimuoviFile`, `aggiornaNomeSoglia`, `aggiornaImpresa`, `_costruisciPreview`, `renderPreview`, `formatNum` devono rimanere identiche.

---

## Task 5: Aggiorna `pages/comparativo.php` — aggiungi SheetJS CDN

**Files:**
- Modify: `comparativo/pages/comparativo.php`

**Step 1: Aggiungere SheetJS prima di ExcelJS**

Trovare il blocco script in fondo alla pagina e aggiungere SheetJS:

```html
    <script>const COMP_ID = <?= $id ?>;</script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/xlsx@0.20.3/dist/xlsx.full.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
    <script src="../assets/js/app.js"></script>
    <script src="../assets/js/sheetjs-reader.js"></script>
    <script src="../assets/js/excel-generator.js"></script>
    <script src="../assets/js/comparativo.js"></script>
```

**Step 2: Verifica**

Ricaricare la pagina comparativo. Aprire DevTools Console — non devono esserci errori di caricamento script.
Controllare Network: devono caricarsi `xlsx.full.min.js`, `exceljs.min.js`, `sheetjs-reader.js`, `excel-generator.js`, `comparativo.js` senza errori 404.

---

## Task 6: Pulizia — rimuovi dipendenza PhpSpreadsheet da genera.php

**Files:**
- Verify: `comparativo/api/genera.php` (già fatto nel Task 2)

**Step 1: Verificare che genera.php non includa più vendor/autoload.php o lettore.php**

Il file non deve più contenere:
- `require_once __DIR__ . '/../vendor/autoload.php'`
- `require_once __DIR__ . '/../core/lettore.php'`
- nessuna chiamata a `leggiCapitolato()`, `leggiPreventivo()`, `estraiFirma()`

Se le trova ancora, rimuoverle.

**Note:** `core/lettore.php` e `core/generatore.php` restano sul server ma non vengono più usati nel flusso principale. Possono essere tenuti per riferimento.

---

## Ordine esecuzione

Tasks 1 e 2 sono indipendenti (possono andare in parallelo).
Task 3 è indipendente da 1 e 2 ma deve essere completato prima di Task 4.
Task 4 dipende da Task 3 (usa `leggiTuttiFile`).
Task 5 dipende da Task 4 (aggiunge il CDN per sheetjs-reader.js).
Task 6 è cleanup finale.

```
[Task 1: servi_file.php] ─┐
[Task 2: genera.php]      ─┤─→ [Task 4: comparativo.js] → [Task 5: comparativo.php] → [Task 6: cleanup]
[Task 3: sheetjs-reader]  ─┘
```
