# Comparativo Preventivi — Web App Implementation Plan

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

**Goal:** Riscrivere il comparativo preventivi come web app PHP + Bootstrap + SQLite, con autenticazione multiruolo, archivio comparativi e preview HTML.

**Architecture:** PHP tradizionale con pagine servite dal server. Bootstrap 5 da CDN per layout responsive. JavaScript vanilla per interazioni AJAX (upload, ricerca live, preview tabella). SQLite come database embedded. PhpSpreadsheet per lettura/scrittura Excel.

**Tech Stack:** PHP 8+, SQLite3 (PDO), PhpSpreadsheet, Bootstrap 5, JavaScript vanilla

**Riferimento design:** `docs/plans/2026-03-03-refactoring-web-design.md`

**File di test Excel:** `FILES/test1/` e `FILES/test2/` (non spostare, servono per testing)

---

## Task 1: Scaffolding — struttura cartelle e dipendenze

**Files:**
- Create: `comparativo/.htaccess`
- Create: `comparativo/config/app.php`
- Create: `comparativo/config/database.php`
- Create: `comparativo/composer.json`
- Create: `comparativo/storage/db/.gitkeep`
- Create: `comparativo/storage/uploads/.gitkeep`
- Create: `comparativo/storage/output/.gitkeep`

**Step 1: Creare la struttura cartelle**

```bash
cd "/Users/paolo/Server/Siti Web/_private/comparativo_cme"
mkdir -p comparativo/{config,core,api,pages,assets/{css,js,img},storage/{db,uploads,output}}
touch comparativo/storage/db/.gitkeep
touch comparativo/storage/uploads/.gitkeep
touch comparativo/storage/output/.gitkeep
```

**Step 2: Creare composer.json**

```json
{
    "require": {
        "phpoffice/phpspreadsheet": "^2.0"
    }
}
```

**Step 3: Installare PhpSpreadsheet**

```bash
cd comparativo
composer install
```

Atteso: cartella `vendor/` creata con PhpSpreadsheet.

**Step 4: Creare config/app.php**

```php
<?php
// Configurazione applicazione — Comparativo Preventivi

// Soglia anomalie (percentuale, modificabile per comparativo)
define('SOGLIA_ANOMALIA_PERC', 20);

// Colori Excel (HEX senza #)
define('COL_HEADER_BG',     '1F4E79');
define('COL_SECTION_BG',    '2E75B6');
define('COL_ANOMALO_ALTO',  'FFCCCC');
define('COL_ANOMALO_BASSO', 'CCFFCC');
define('COL_MEDIA_BG',      'E2EFDA');
define('COL_MANCANTE_BG',   'FFF2CC');
define('FONT_NAME',         'Arial');

// Upload
define('MAX_UPLOAD_SIZE', 10 * 1024 * 1024); // 10 MB
define('ALLOWED_EXTENSIONS', ['xlsx', 'xls']);

// Sessione
define('SESSION_TIMEOUT', 7200); // 2 ore

// Percorsi
define('BASE_PATH', dirname(__DIR__));
define('STORAGE_PATH', BASE_PATH . '/storage');
define('UPLOADS_PATH', STORAGE_PATH . '/uploads');
define('OUTPUT_PATH',  STORAGE_PATH . '/output');
define('DB_PATH',      STORAGE_PATH . '/db/comparativo.sqlite');

// Credits sviluppatore
define('DEV_NAME',     'DUEBYTES di Paolo Alberti');
define('DEV_URL',      'https://www.duebytes.it/');
define('DEV_LOGO_URL', 'https://www.duebytes.it/assets/img/duebytes.png');
```

**Step 5: Creare config/database.php**

```php
<?php
// Connessione SQLite — singleton PDO

function getDB(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        require_once __DIR__ . '/app.php';
        $pdo = new PDO('sqlite:' . DB_PATH);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        $pdo->exec('PRAGMA journal_mode = WAL');
        $pdo->exec('PRAGMA foreign_keys = ON');
        inizializzaDB($pdo);
    }
    return $pdo;
}

function inizializzaDB(PDO $pdo): void {
    // Crea le tabelle se non esistono (primo avvio)
    $pdo->exec("
        CREATE TABLE IF NOT EXISTS utenti (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            username        TEXT UNIQUE NOT NULL,
            password        TEXT NOT NULL,
            ruolo           TEXT NOT NULL DEFAULT 'utente',
            attivo          INTEGER NOT NULL DEFAULT 1,
            creato_da       INTEGER,
            creato_il       TEXT NOT NULL,
            ultimo_accesso  TEXT
        );

        CREATE TABLE IF NOT EXISTS comparativi (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            nome            TEXT NOT NULL,
            soglia          INTEGER NOT NULL DEFAULT 20,
            utente_id       INTEGER NOT NULL,
            creato_il       TEXT NOT NULL,
            aggiornato_il   TEXT NOT NULL,
            stato           TEXT NOT NULL DEFAULT 'bozza',
            FOREIGN KEY (utente_id) REFERENCES utenti(id)
        );

        CREATE TABLE IF NOT EXISTS file_comparativo (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            comparativo_id  INTEGER NOT NULL,
            tipo            TEXT NOT NULL,
            nome_impresa    TEXT,
            nome_file       TEXT NOT NULL,
            percorso        TEXT NOT NULL,
            caricato_il     TEXT NOT NULL,
            voci_prezzate   INTEGER,
            voci_totali     INTEGER,
            FOREIGN KEY (comparativo_id) REFERENCES comparativi(id)
        );

        CREATE TABLE IF NOT EXISTS log (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            timestamp       TEXT NOT NULL,
            livello         TEXT NOT NULL,
            utente_id       INTEGER,
            azione          TEXT NOT NULL,
            dettagli        TEXT,
            ip              TEXT
        );
    ");

    // Crea utente developer se non esiste
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM utenti WHERE ruolo = ?');
    $stmt->execute(['developer']);
    if ($stmt->fetchColumn() == 0) {
        $stmt = $pdo->prepare(
            'INSERT INTO utenti (username, password, ruolo, creato_il)
             VALUES (?, ?, ?, ?)'
        );
        $stmt->execute([
            'developer',
            password_hash('developer', PASSWORD_BCRYPT),
            'developer',
            date('Y-m-d H:i:s')
        ]);
    }
}
```

**Step 6: Creare .htaccess**

```apache
# Protezione cartelle sensibili
<IfModule mod_rewrite.c>
    RewriteEngine On

    # Blocca accesso diretto a storage/, config/, core/
    RewriteRule ^storage/ - [F,L]
    RewriteRule ^config/ - [F,L]
    RewriteRule ^core/ - [F,L]
</IfModule>

# Impedisci listing directory
Options -Indexes

# Charset
AddDefaultCharset UTF-8
```

**Step 7: Verificare che il setup funzioni**

```bash
cd comparativo
php -r "require 'config/database.php'; \$db = getDB(); echo 'DB OK: ' . DB_PATH . PHP_EOL;"
```

Atteso: `DB OK: .../storage/db/comparativo.sqlite` e file .sqlite creato.

---

## Task 2: Autenticazione — core/auth.php + login

**Files:**
- Create: `comparativo/core/auth.php`
- Create: `comparativo/core/logger.php`
- Create: `comparativo/index.php`
- Create: `comparativo/api/login.php`
- Create: `comparativo/api/logout.php`

**Step 1: Creare core/logger.php**

```php
<?php
// Sistema di logging su SQLite

require_once __DIR__ . '/../config/database.php';

function logAzione(string $livello, string $azione, ?string $dettagli = null, ?int $utenteId = null): void {
    try {
        $db = getDB();
        $stmt = $db->prepare(
            'INSERT INTO log (timestamp, livello, utente_id, azione, dettagli, ip)
             VALUES (?, ?, ?, ?, ?, ?)'
        );
        $stmt->execute([
            date('Y-m-d H:i:s'),
            $livello,
            $utenteId,
            $azione,
            $dettagli,
            $_SERVER['REMOTE_ADDR'] ?? null
        ]);
    } catch (Exception $e) {
        error_log("Errore logger: " . $e->getMessage());
    }
}
```

**Step 2: Creare core/auth.php**

```php
<?php
// Autenticazione e gestione sessioni

session_start();

require_once __DIR__ . '/../config/app.php';
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/logger.php';

function utenteLoggato(): bool {
    if (!isset($_SESSION['utente_id'])) return false;
    // Verifica timeout sessione
    if (isset($_SESSION['ultimo_accesso']) &&
        time() - $_SESSION['ultimo_accesso'] > SESSION_TIMEOUT) {
        session_destroy();
        return false;
    }
    $_SESSION['ultimo_accesso'] = time();
    return true;
}

function richiedeLogin(): void {
    if (!utenteLoggato()) {
        header('Location: ' . dirname($_SERVER['SCRIPT_NAME']) . '/../index.php');
        exit;
    }
}

function richiedeRuolo(string ...$ruoli): void {
    richiedeLogin();
    if (!in_array($_SESSION['ruolo'], $ruoli)) {
        http_response_code(403);
        echo json_encode(['errore' => 'Permessi insufficienti']);
        exit;
    }
}

function tentaLogin(string $username, string $password): array {
    $db = getDB();
    $stmt = $db->prepare('SELECT * FROM utenti WHERE username = ? AND attivo = 1');
    $stmt->execute([$username]);
    $utente = $stmt->fetch();

    if (!$utente || !password_verify($password, $utente['password'])) {
        logAzione('warning', 'login_fallito', "Username: $username");
        return ['ok' => false, 'errore' => 'Credenziali non valide'];
    }

    // Rigenerazione ID sessione (sicurezza)
    session_regenerate_id(true);
    $_SESSION['utente_id'] = $utente['id'];
    $_SESSION['username']  = $utente['username'];
    $_SESSION['ruolo']     = $utente['ruolo'];
    $_SESSION['ultimo_accesso'] = time();

    // Aggiorna ultimo accesso nel DB
    $stmt = $db->prepare('UPDATE utenti SET ultimo_accesso = ? WHERE id = ?');
    $stmt->execute([date('Y-m-d H:i:s'), $utente['id']]);

    logAzione('info', 'login', null, $utente['id']);
    return ['ok' => true, 'ruolo' => $utente['ruolo']];
}

function logout(): void {
    $utenteId = $_SESSION['utente_id'] ?? null;
    if ($utenteId) logAzione('info', 'logout', null, $utenteId);
    session_destroy();
}

// CSRF token
function generaCSRF(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function verificaCSRF(string $token): bool {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
```

**Step 3: Creare api/login.php**

```php
<?php
require_once __DIR__ . '/../core/auth.php';

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

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

$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';

if (!$username || !$password) {
    echo json_encode(['ok' => false, 'errore' => 'Compila tutti i campi']);
    exit;
}

echo json_encode(tentaLogin($username, $password));
```

**Step 4: Creare api/logout.php**

```php
<?php
require_once __DIR__ . '/../core/auth.php';

logout();
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
```

**Step 5: Creare index.php (pagina di login)**

```php
<?php
require_once __DIR__ . '/config/app.php';
session_start();
// Se gia loggato, vai alla dashboard
if (isset($_SESSION['utente_id'])) {
    header('Location: pages/dashboard.php');
    exit;
}
?>
<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Comparativo Preventivi — Login</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="assets/css/style.css" rel="stylesheet">
</head>
<body class="bg-light d-flex flex-column min-vh-100">

    <!-- Header studio -->
    <div class="bg-header text-white text-center py-4">
        <h4 class="mb-1 fw-bold">COMPARATIVO PREVENTIVI</h4>
        <small class="text-white-50">Calvi Ceschia Vigano / Architetti Associati</small>
    </div>

    <!-- Form login -->
    <div class="flex-grow-1 d-flex align-items-center justify-content-center">
        <div class="card shadow-sm" style="width: 100%; max-width: 400px;">
            <div class="card-body p-4">
                <h5 class="text-accent fw-bold mb-3">Accedi</h5>
                <div id="errore-login" class="alert alert-danger d-none" role="alert"></div>
                <form id="form-login">
                    <div class="mb-3">
                        <label for="username" class="form-label">Username</label>
                        <input type="text" class="form-control" id="username" autocomplete="username" required>
                    </div>
                    <div class="mb-3">
                        <label for="password" class="form-label">Password</label>
                        <input type="password" class="form-control" id="password" autocomplete="current-password" required>
                    </div>
                    <button type="submit" class="btn btn-accent w-100 fw-bold">ACCEDI</button>
                </form>
            </div>
        </div>
    </div>

    <!-- Footer credits DUEBYTES -->
    <footer class="text-center py-3 border-top bg-white">
        <a href="<?= DEV_URL ?>" target="_blank" class="text-decoration-none text-muted">
            <img src="<?= DEV_LOGO_URL ?>" alt="DUEBYTES" height="20" class="me-1">
            di Paolo Alberti
        </a>
    </footer>

    <script>
    document.getElementById('form-login').addEventListener('submit', async (e) => {
        e.preventDefault();
        const errBox = document.getElementById('errore-login');
        errBox.classList.add('d-none');

        const resp = await fetch('api/login.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
                username: document.getElementById('username').value,
                password: document.getElementById('password').value
            })
        });
        const data = await resp.json();
        if (data.ok) {
            window.location.href = 'pages/dashboard.php';
        } else {
            errBox.textContent = data.errore;
            errBox.classList.remove('d-none');
        }
    });
    </script>
</body>
</html>
```

**Step 6: Creare assets/css/style.css**

```css
/* Palette — Calvi Ceschia Vigano */
:root {
    --header-bg: #1F4E79;
    --accent: #4F8EF7;
    --accent-hover: #3A7AE8;
    --body-bg: #F5F6FA;
    --card-bg: #FFFFFF;
    --text: #1A1A2E;
    --text-muted: #6B7280;
    --border: #E5E7EB;
}

body { background-color: var(--body-bg); color: var(--text); }

.bg-header { background-color: var(--header-bg); }
.text-accent { color: var(--accent); }
.btn-accent {
    background-color: var(--accent);
    border-color: var(--accent);
    color: #fff;
}
.btn-accent:hover {
    background-color: var(--accent-hover);
    border-color: var(--accent-hover);
    color: #fff;
}

.navbar { background-color: var(--header-bg) !important; }
```

**Step 7: Testare il login nel browser**

Aprire `http://localhost/comparativo_cme/comparativo/` nel browser.
Atteso: pagina di login con form, header blu scuro, footer DUEBYTES.
Login con developer/developer -> redirect a dashboard (che ancora non esiste, 404 atteso).

---

## Task 3: Layout base — navbar + dashboard vuota

**Files:**
- Create: `comparativo/pages/dashboard.php`
- Create: `comparativo/assets/js/app.js`

**Step 1: Creare assets/js/app.js**

```js
// Utility globali

// Logout
async function logout() {
    await fetch('../api/logout.php');
    window.location.href = '../index.php';
}

// Fetch con gestione errori
async function apiFetch(url, options = {}) {
    try {
        const resp = await fetch(url, {
            headers: { 'Content-Type': 'application/json', ...options.headers },
            ...options
        });
        if (resp.status === 403) {
            alert('Permessi insufficienti');
            return null;
        }
        return await resp.json();
    } catch (e) {
        console.error('Errore API:', e);
        alert('Errore di comunicazione con il server');
        return null;
    }
}

// Escape HTML per prevenire XSS
function esc(str) {
    const div = document.createElement('div');
    div.textContent = str ?? '';
    return div.innerHTML;
}

// Formattazione data IT
function dataIT(iso) {
    if (!iso) return '—';
    const d = new Date(iso.replace(' ', 'T'));
    return d.toLocaleDateString('it-IT') + ' ' + d.toLocaleTimeString('it-IT', {hour:'2-digit', minute:'2-digit'});
}
```

**Step 2: Creare pages/dashboard.php**

Pagina con navbar, barra ricerca, tabella comparativi (inizialmente vuota),
bottone "Nuovo comparativo". Include la navbar condivisa.

```php
<?php
require_once __DIR__ . '/../core/auth.php';
richiedeLogin();
$ruolo = $_SESSION['ruolo'];
?>
<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Comparativi — Dashboard</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
    <link href="../assets/css/style.css" rel="stylesheet">
</head>
<body class="d-flex flex-column min-vh-100">

    <!-- Navbar -->
    <nav class="navbar navbar-expand-lg navbar-dark">
        <div class="container-fluid px-4">
            <span class="navbar-brand fw-bold">COMPARATIVO PREVENTIVI</span>
            <span class="navbar-text text-white-50 d-none d-md-inline ms-2">
                Calvi Ceschia Vigano / Architetti Associati
            </span>
            <div class="ms-auto d-flex align-items-center gap-3">
                <a href="dashboard.php" class="text-white text-decoration-none fw-bold">Comparativi</a>
                <?php if (in_array($ruolo, ['admin', 'developer'])): ?>
                    <a href="utenti.php" class="text-white-50 text-decoration-none">Utenti</a>
                <?php endif; ?>
                <?php if ($ruolo === 'developer'): ?>
                    <a href="log.php" class="text-white-50 text-decoration-none">Log</a>
                <?php endif; ?>
                <span class="text-white-50 small"><?= htmlspecialchars($_SESSION['username']) ?></span>
                <a href="#" onclick="logout(); return false;" class="text-white-50 text-decoration-none">
                    <i class="bi bi-box-arrow-right"></i>
                </a>
            </div>
        </div>
    </nav>

    <!-- Contenuto -->
    <div class="container-fluid px-4 py-4 flex-grow-1">
        <div class="d-flex justify-content-between align-items-center mb-3">
            <h5 class="text-accent fw-bold mb-0">I TUOI COMPARATIVI</h5>
            <button class="btn btn-accent btn-sm fw-bold" onclick="nuovoComparativo()">
                <i class="bi bi-plus-lg"></i> Nuovo comparativo
            </button>
        </div>

        <!-- Barra ricerca -->
        <div class="mb-3">
            <input type="text" class="form-control" id="ricerca"
                   placeholder="Cerca comparativo per nome..."
                   oninput="filtraTabella()">
        </div>

        <!-- Tabella comparativi -->
        <div class="card shadow-sm">
            <div class="table-responsive">
                <table class="table table-hover mb-0" id="tabella-comparativi">
                    <thead class="table-light">
                        <tr>
                            <th>Nome</th>
                            <th>Data creazione</th>
                            <th>Utente</th>
                            <th>Stato</th>
                            <th>Imprese</th>
                            <th class="text-end">Azioni</th>
                        </tr>
                    </thead>
                    <tbody id="corpo-tabella">
                        <tr><td colspan="6" class="text-muted text-center py-4">Caricamento...</td></tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>

    <!-- Footer -->
    <footer class="text-center py-3 border-top bg-white mt-auto">
        <a href="<?= DEV_URL ?>" target="_blank" class="text-decoration-none text-muted small">
            <img src="<?= DEV_LOGO_URL ?>" alt="DUEBYTES" height="16" class="me-1">
            di Paolo Alberti
        </a>
    </footer>

    <script src="../assets/js/app.js"></script>
    <script>
    // Carica lista comparativi
    async function caricaComparativi() {
        const data = await apiFetch('../api/comparativi.php');
        const tbody = document.getElementById('corpo-tabella');
        if (!data || !data.lista || data.lista.length === 0) {
            tbody.innerHTML = '<tr><td colspan="6" class="text-muted text-center py-4">Nessun comparativo. Creane uno nuovo!</td></tr>';
            return;
        }
        tbody.innerHTML = data.lista.map(c => `
            <tr data-nome="${esc(c.nome).toLowerCase()}">
                <td><a href="comparativo.php?id=${c.id}" class="fw-bold text-decoration-none">${esc(c.nome)}</a></td>
                <td class="text-muted small">${dataIT(c.creato_il)}</td>
                <td class="small">${esc(c.username)}</td>
                <td><span class="badge ${c.stato === 'generato' ? 'bg-success' : 'bg-secondary'}">${esc(c.stato)}</span></td>
                <td class="text-center">${c.n_imprese}</td>
                <td class="text-end">
                    <a href="comparativo.php?id=${c.id}" class="btn btn-sm btn-outline-primary">Apri</a>
                    <button class="btn btn-sm btn-outline-danger" onclick="eliminaComparativo(${c.id})">
                        <i class="bi bi-trash"></i>
                    </button>
                </td>
            </tr>
        `).join('');
    }

    // Filtro ricerca live
    function filtraTabella() {
        const query = document.getElementById('ricerca').value.toLowerCase();
        document.querySelectorAll('#corpo-tabella tr[data-nome]').forEach(tr => {
            tr.style.display = tr.dataset.nome.includes(query) ? '' : 'none';
        });
    }

    // Nuovo comparativo
    async function nuovoComparativo() {
        const nome = prompt('Nome del comparativo:');
        if (!nome || !nome.trim()) return;
        const data = await apiFetch('../api/comparativi.php', {
            method: 'POST',
            body: JSON.stringify({ azione: 'crea', nome: nome.trim() })
        });
        if (data && data.ok) {
            window.location.href = 'comparativo.php?id=' + data.id;
        }
    }

    // Elimina comparativo
    async function eliminaComparativo(id) {
        if (!confirm('Eliminare questo comparativo e tutti i suoi file?')) return;
        const data = await apiFetch('../api/comparativi.php', {
            method: 'POST',
            body: JSON.stringify({ azione: 'elimina', id: id })
        });
        if (data && data.ok) caricaComparativi();
    }

    // Avvio
    caricaComparativi();
    </script>
</body>
</html>
```

**Step 3: Testare nel browser**

Login -> dashboard con tabella vuota e bottone "Nuovo comparativo".

---

## Task 4: API comparativi — CRUD

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

**Step 1: Creare api/comparativi.php**

Endpoint che gestisce lista (GET) e azioni crea/elimina (POST).

```php
<?php
require_once __DIR__ . '/../core/auth.php';
richiedeLogin();

header('Content-Type: application/json');
$db = getDB();

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    // Lista comparativi (con conteggio imprese e username creatore)
    $stmt = $db->query("
        SELECT c.*, u.username,
            (SELECT COUNT(*) FROM file_comparativo f
             WHERE f.comparativo_id = c.id AND f.tipo = 'preventivo') AS n_imprese
        FROM comparativi c
        JOIN utenti u ON u.id = c.utente_id
        ORDER BY c.creato_il DESC
    ");
    echo json_encode(['ok' => true, 'lista' => $stmt->fetchAll()]);
    exit;
}

// POST: azioni (crea, elimina, aggiorna)
$input = json_decode(file_get_contents('php://input'), true);
$azione = $input['azione'] ?? '';

switch ($azione) {
    case 'crea':
        $nome = trim($input['nome'] ?? '');
        if (!$nome) {
            echo json_encode(['ok' => false, 'errore' => 'Nome obbligatorio']);
            exit;
        }
        $now = date('Y-m-d H:i:s');
        $stmt = $db->prepare(
            'INSERT INTO comparativi (nome, utente_id, creato_il, aggiornato_il)
             VALUES (?, ?, ?, ?)'
        );
        $stmt->execute([$nome, $_SESSION['utente_id'], $now, $now]);
        $id = $db->lastInsertId();
        logAzione('info', 'comparativo_creato', "ID: $id, Nome: $nome", $_SESSION['utente_id']);
        echo json_encode(['ok' => true, 'id' => $id]);
        break;

    case 'elimina':
        $id = intval($input['id'] ?? 0);
        // Elimina file fisici
        $uploadDir = UPLOADS_PATH . "/$id";
        $outputDir = OUTPUT_PATH . "/$id";
        if (is_dir($uploadDir)) {
            array_map('unlink', glob("$uploadDir/*"));
            rmdir($uploadDir);
        }
        if (is_dir($outputDir)) {
            array_map('unlink', glob("$outputDir/*"));
            rmdir($outputDir);
        }
        // Elimina da DB
        $db->prepare('DELETE FROM file_comparativo WHERE comparativo_id = ?')->execute([$id]);
        $db->prepare('DELETE FROM comparativi WHERE id = ?')->execute([$id]);
        logAzione('info', 'comparativo_eliminato', "ID: $id", $_SESSION['utente_id']);
        echo json_encode(['ok' => true]);
        break;

    case 'aggiorna':
        $id     = intval($input['id'] ?? 0);
        $nome   = trim($input['nome'] ?? '');
        $soglia = intval($input['soglia'] ?? 20);
        $stmt = $db->prepare(
            'UPDATE comparativi SET nome = ?, soglia = ?, aggiornato_il = ? WHERE id = ?'
        );
        $stmt->execute([$nome, $soglia, date('Y-m-d H:i:s'), $id]);
        echo json_encode(['ok' => true]);
        break;

    default:
        echo json_encode(['ok' => false, 'errore' => 'Azione sconosciuta']);
}
```

**Step 2: Testare**

Dal browser: creare un comparativo, verificare che appaia nella tabella, eliminarlo.

---

## Task 5: Upload file Excel

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

**Step 1: Creare api/upload.php**

```php
<?php
require_once __DIR__ . '/../core/auth.php';
richiedeLogin();

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

$comparativoId = intval($_POST['comparativo_id'] ?? 0);
$tipo           = $_POST['tipo'] ?? '';  // 'capitolato' o 'preventivo'
$nomeImpresa    = trim($_POST['nome_impresa'] ?? '');

if (!$comparativoId || !in_array($tipo, ['capitolato', 'preventivo'])) {
    echo json_encode(['ok' => false, 'errore' => 'Parametri mancanti']);
    exit;
}

if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
    echo json_encode(['ok' => false, 'errore' => 'Errore nel caricamento del file']);
    exit;
}

$file = $_FILES['file'];

// Validazione estensione
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXTENSIONS)) {
    echo json_encode(['ok' => false, 'errore' => 'Formato non supportato. Usa .xlsx o .xls']);
    exit;
}

// Validazione dimensione
if ($file['size'] > MAX_UPLOAD_SIZE) {
    echo json_encode(['ok' => false, 'errore' => 'File troppo grande (max 10MB)']);
    exit;
}

// Crea cartella upload per questo comparativo
$dir = UPLOADS_PATH . "/$comparativoId";
if (!is_dir($dir)) mkdir($dir, 0755, true);

// Nome file sicuro
$nomeFileOriginale = $file['name'];
$nomeSicuro = preg_replace('/[^a-zA-Z0-9._-]/', '_', $nomeFileOriginale);
$percorso = "$dir/$nomeSicuro";

// Se capitolato, rimuovi eventuale precedente
if ($tipo === 'capitolato') {
    $db = getDB();
    $vecchi = $db->prepare('SELECT percorso FROM file_comparativo WHERE comparativo_id = ? AND tipo = ?');
    $vecchi->execute([$comparativoId, 'capitolato']);
    foreach ($vecchi->fetchAll() as $v) {
        if (file_exists($v['percorso'])) unlink($v['percorso']);
    }
    $db->prepare('DELETE FROM file_comparativo WHERE comparativo_id = ? AND tipo = ?')
       ->execute([$comparativoId, 'capitolato']);
}

// Sposta il file
if (!move_uploaded_file($file['tmp_name'], $percorso)) {
    echo json_encode(['ok' => false, 'errore' => 'Errore nel salvataggio del file']);
    exit;
}

// Salva nel DB
$db = getDB();
$stmt = $db->prepare(
    'INSERT INTO file_comparativo (comparativo_id, tipo, nome_impresa, nome_file, percorso, caricato_il)
     VALUES (?, ?, ?, ?, ?, ?)'
);
$stmt->execute([$comparativoId, $tipo, $nomeImpresa ?: null, $nomeFileOriginale, $percorso, date('Y-m-d H:i:s')]);
$fileId = $db->lastInsertId();

logAzione('info', 'file_caricato', "Tipo: $tipo, File: $nomeFileOriginale", $_SESSION['utente_id']);

echo json_encode([
    'ok' => true,
    'file_id' => $fileId,
    'nome_file' => $nomeFileOriginale
]);
```

**Step 2: Testare con un upload manuale (curl o browser)**

---

## Task 6: Core PHP — Lettore (traduzione da Python)

**Files:**
- Create: `comparativo/core/lettore.php`

**Step 1: Creare core/lettore.php**

Traduzione 1:1 da `_archivio_python/core/lettore.py`.
Stessa logica: col D numerico -> riga dati, tutto il resto -> testo/vuota.
Usa PhpSpreadsheet.

Il file deve contenere:
- `trovaRigaHash($ws)` -> equivalente di `_trova_riga_hash()`
- `estraiFirma($path)` -> equivalente di `estrai_firma_intestazione()`
- `trovaRigaIntestazione($ws, $firma)` -> equivalente di `trova_riga_intestazione()`
- `leggiCapitolato($path)` -> equivalente di `leggi_capitolato()`
- `leggiPreventivo($path, $firma, $numeriVoce)` -> equivalente di `leggi_preventivo()`

Riferimento Python: `_archivio_python/core/lettore.py`

**Step 2: Testare con uno script PHP di verifica**

```bash
cd comparativo
php -r "
require 'vendor/autoload.php';
require 'core/lettore.php';
\$s = leggiCapitolato('../FILES/test2/Capitolato vuoto.xlsx');
\$dati = array_filter(\$s, fn(\$r) => \$r['tipo'] === 'dati');
echo 'Righe totali: ' . count(\$s) . PHP_EOL;
echo 'Righe dati: ' . count(\$dati) . PHP_EOL;
"
```

Atteso: ~250 righe totali, 96 righe dati (come il test Python).

---

## Task 7: Core PHP — Generatore (traduzione da Python)

**Files:**
- Create: `comparativo/core/generatore.php`

**Step 1: Creare core/generatore.php**

Traduzione 1:1 da `_archivio_python/core/generatore.py`.
Contiene:
- `generaComparativo($capitolatoPath, $prevPaths, $nomi, $outputPath, $soglia)` -> funzione principale
- `foglioComparativo($ws, $struttura, $imprese, $medie, $nImprese, $soglia)` -> foglio comparativo
- `foglioRiepilogo($ws, $struttura, $imprese, $medie, $nImprese)` -> foglio riepilogo

Stessi colori, stesse formule Excel, stessa struttura output del Python.

Riferimento Python: `_archivio_python/core/generatore.py`

**Step 2: Testare end-to-end**

```bash
cd comparativo
php -r "
require 'vendor/autoload.php';
require 'core/lettore.php';
require 'core/generatore.php';
require 'config/app.php';
generaComparativo(
    '../FILES/test2/Capitolato vuoto.xlsx',
    ['../FILES/test2/Brugnotto.xlsx', '../FILES/test2/Byld.xlsx'],
    ['Brugnotto', 'Byld'],
    '/tmp/test_php_output.xlsx',
    20
);
echo 'File generato OK' . PHP_EOL;
"
```

Atteso: file `/tmp/test_php_output.xlsx` generato, comparabile al test Python.

**Step 3: Testare anche con test1**

```bash
php -r "
require 'vendor/autoload.php';
require 'core/lettore.php';
require 'core/generatore.php';
require 'config/app.php';
generaComparativo(
    '../FILES/test1/Capitolato BASE VUOTO.xlsx',
    ['../FILES/test1/brc.xlsx', '../FILES/test1/conio.xlsx', '../FILES/test1/gio.xlsx'],
    ['BRC', 'CONIO', 'GIO'],
    '/tmp/test1_php_output.xlsx',
    20
);
echo 'Test1 OK' . PHP_EOL;
"
```

---

## Task 8: API genera + download

**Files:**
- Create: `comparativo/api/genera.php`
- Create: `comparativo/api/download.php`

**Step 1: Creare api/genera.php**

Endpoint che:
1. Legge comparativo_id dal POST
2. Recupera dal DB il capitolato e i preventivi associati
3. Chiama generaComparativo()
4. Salva l'Excel in storage/output/{id}/
5. Aggiorna stato comparativo a 'generato'
6. Restituisce i dati per la preview HTML

**Step 2: Creare api/download.php**

Endpoint che:
1. Riceve comparativo_id via GET
2. Trova l'Excel generato in storage/output/{id}/
3. Lo invia come download con Content-Disposition

---

## Task 9: Pagina dettaglio comparativo

**Files:**
- Create: `comparativo/pages/comparativo.php`
- Create: `comparativo/assets/js/comparativo.js`

**Step 1: Creare pages/comparativo.php**

Pagina con:
- Nome comparativo editabile
- Soglia anomalie (spinbox)
- Sezione upload capitolato
- Sezione upload preventivi (multipli, con nome impresa)
- Bottone "Genera comparativo"
- Area preview HTML della tabella (dopo generazione)
- Bottone download Excel

**Step 2: Creare assets/js/comparativo.js**

JavaScript per:
- Upload file con FormData + fetch (progress bar opzionale)
- Aggiornamento nome/soglia via AJAX
- Chiamata genera con visualizzazione preview HTML
- Download Excel

**Step 3: Testare il flusso completo nel browser**

1. Dashboard -> Nuovo comparativo -> inserire nome
2. Caricare capitolato test2
3. Caricare Brugnotto.xlsx e Byld.xlsx con nomi impresa
4. Generare -> verificare preview HTML con colori anomalie
5. Scaricare Excel -> verificare che sia corretto

---

## Task 10: Gestione utenti

**Files:**
- Create: `comparativo/api/utenti.php`
- Create: `comparativo/pages/utenti.php`
- Create: `comparativo/assets/js/utenti.js`

**Step 1: Creare api/utenti.php**

CRUD utenti con controllo ruoli:
- GET: lista utenti (admin vede solo utenti, developer vede tutti)
- POST azione=crea: crea utente (admin crea utenti, developer crea admin+utenti)
- POST azione=modifica: modifica username/ruolo/attivo
- POST azione=cambio_password: cambia password (utente cambia la propria, admin/dev cambiano altri)

**Step 2: Creare pages/utenti.php**

Tabella utenti + form modale Bootstrap per creare/modificare.

**Step 3: Creare assets/js/utenti.js**

CRUD via AJAX + aggiornamento tabella.

**Step 4: Testare**

Login developer -> Utenti -> creare admin -> creare utente -> disabilitare -> riabilitare.

---

## Task 11: Pagina Log (developer)

**Files:**
- Create: `comparativo/api/log.php`
- Create: `comparativo/pages/log.php`

**Step 1: Creare api/log.php**

```php
<?php
require_once __DIR__ . '/../core/auth.php';
richiedeRuolo('developer');

header('Content-Type: application/json');
$db = getDB();

$livello = $_GET['livello'] ?? '';
$pagina  = max(1, intval($_GET['pagina'] ?? 1));
$perPagina = 50;
$offset = ($pagina - 1) * $perPagina;

$where = '1=1';
$params = [];
if ($livello && in_array($livello, ['info', 'warning', 'errore'])) {
    $where .= ' AND l.livello = ?';
    $params[] = $livello;
}

$stmt = $db->prepare("
    SELECT l.*, u.username
    FROM log l
    LEFT JOIN utenti u ON u.id = l.utente_id
    WHERE $where
    ORDER BY l.timestamp DESC
    LIMIT ? OFFSET ?
");
$params[] = $perPagina;
$params[] = $offset;
$stmt->execute($params);

echo json_encode(['ok' => true, 'lista' => $stmt->fetchAll()]);
```

**Step 2: Creare pages/log.php**

Tabella con filtri per livello e paginazione. Solo accessibile da developer.

---

## Task 12: Pulizia e test finale

**Step 1: Aggiornare CLAUDE.md con la nuova struttura**

**Step 2: Aggiornare la memoria interna**

**Step 3: Test completo end-to-end nel browser**

1. Login developer/developer
2. Creare un comparativo con files test2
3. Generare -> preview HTML -> download Excel
4. Verificare che l'Excel sia identico al test Python
5. Creare un admin
6. Login come admin -> verificare che non veda Log
7. Creare un utente
8. Login come utente -> verificare permessi limitati
9. Verificare log come developer

**Step 4: Test con files test1 (capitolato grande)**

1. Creare comparativo con Capitolato BASE VUOTO + brc + conio + gio
2. Generare -> verificare che funzioni con file grandi

---

## Riepilogo task

| # | Task | Cosa |
|---|---|---|
| 1 | Scaffolding | Cartelle, Composer, config, .htaccess |
| 2 | Autenticazione | auth.php, login, logout, sessioni |
| 3 | Layout base | Navbar, dashboard vuota |
| 4 | API comparativi | CRUD lista/crea/elimina |
| 5 | Upload | Caricamento file Excel |
| 6 | Core Lettore | Traduzione lettore.py -> PHP |
| 7 | Core Generatore | Traduzione generatore.py -> PHP |
| 8 | API genera/download | Generazione + download Excel |
| 9 | Pagina comparativo | Dettaglio con upload, preview, download |
| 10 | Gestione utenti | CRUD utenti con ruoli |
| 11 | Pagina Log | Visualizzazione log per developer |
| 12 | Pulizia e test | Test E2E, aggiornamento docs |
