# Toto Implementation Plan

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

**Goal:** Web app SvelteKit per Salvatore (Circet) che gestisce import e modifica di estrazioni Excel di produzione FTTH, una tabella per ogni comune in cui lavora.

**Architecture:** SvelteKit 2 + Svelte 5, deploy Vercel Hobby, database Turso (SQLite). Auth custom session-based single-user. Import wipe & replace per comune. Tabella editabile con paginazione/filtri/edit inline + drawer dettaglio per le 54 colonne.

**Tech Stack:** SvelteKit 2 · Svelte 5 · TypeScript · Vite · Turso `@libsql/client` · `xlsx` (SheetJS) · `bcryptjs` · Vitest · Playwright (smoke E2E opzionale).

**Riferimento design:** `docs/plans/2026-05-02-toto-design.md`

---

## Convenzioni

- **Cwd:** `/Users/paolo/Server/Siti Web/_private/Toto`. Tutti i comandi shell partono da qui salvo diversa indicazione.
- **Git:** la directory non è ancora un repo. La inizializziamo nella **Task 0**.
- **Commit:** uno per task (a meno che la task espliciti diversamente). Messaggi conventional commits (`feat:`, `fix:`, `chore:`, `test:`, `docs:`).
- **Test runner:** Vitest per unit (`npm test`), Playwright opzionale per smoke E2E (`npm run e2e`).
- **TDD:** dove ha senso (logica server, parsing, repositories). Per UI Svelte usiamo test integrati con `@testing-library/svelte` solo per i componenti complessi (DataTable, ColumnPicker).

---

## Fase 0 — Setup repository

### Task 0.1: Init repo git

**Files:**
- Create: `.gitignore`

**Step 1: Inizializza git e crea .gitignore**

```bash
git init -b main
```

Crea `.gitignore`:

```
node_modules/
.svelte-kit/
build/
.env
.env.*
!.env.example
.vercel/
*.log
.DS_Store
playwright-report/
test-results/
coverage/
```

**Step 2: First commit**

```bash
git add .gitignore docs/
git commit -m "chore: initial repo with design docs"
```

---

### Task 0.2: Scaffold progetto SvelteKit

**Files:**
- Create: `package.json`, `svelte.config.js`, `vite.config.ts`, `tsconfig.json`, `src/app.html`, `src/app.d.ts`, `src/routes/+layout.svelte`, `src/routes/+page.svelte`

**Step 1: Crea progetto SvelteKit con TypeScript via CLI ufficiale**

```bash
npx sv create . --template minimal --types ts --no-add-ons --install npm
```

Se chiede conferma per directory non vuota → confermare (i file `docs/` non vengono toccati).

**Step 2: Verifica avvio dev server**

```bash
npm run dev
```

Aprire `http://localhost:5173`. Atteso: pagina vuota di SvelteKit. Stoppa con Ctrl+C.

**Step 3: Commit**

```bash
git add -A
git commit -m "chore: scaffold SvelteKit project"
```

---

### Task 0.3: Installa dipendenze

**Step 1: Installa runtime deps**

```bash
npm install @libsql/client xlsx bcryptjs
npm install -D @types/bcryptjs vitest @vitest/ui @testing-library/svelte @testing-library/jest-dom jsdom
```

**Step 2: Aggiungi script di test al package.json**

Modifica `package.json` aggiungendo nel `scripts`:

```json
"test": "vitest run",
"test:watch": "vitest",
"migrate": "node --import tsx scripts/migrate.ts"
```

E aggiungi `tsx` come dev dep:

```bash
npm install -D tsx
```

**Step 3: Configura Vitest**

Modifica `vite.config.ts`:

```ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  plugins: [sveltekit()],
  test: {
    include: ['src/**/*.{test,spec}.{js,ts}'],
    environment: 'node'
  }
});
```

**Step 4: Commit**

```bash
git add package.json package-lock.json vite.config.ts
git commit -m "chore: install runtime deps and configure vitest"
```

---

### Task 0.4: File ambiente

**Files:**
- Create: `.env.example`, `.env`

`.env.example`:

```
TURSO_DB_URL=libsql://your-db.turso.io
TURSO_AUTH_TOKEN=your-token
COOKIE_SECRET=change-me-32-bytes-base64
INITIAL_USER_EMAIL=salvatore.damato@circet.it
INITIAL_USER_PASSWORD=admin
```

`.env` (copia di `.env.example`, per dev locale userai un DB Turso di sviluppo separato — vedi Task 1.0):

```
TURSO_DB_URL=
TURSO_AUTH_TOKEN=
COOKIE_SECRET=dev-secret-change-me-only-for-local
INITIAL_USER_EMAIL=salvatore.damato@circet.it
INITIAL_USER_PASSWORD=admin
```

**Step 2: Commit**

```bash
git add .env.example
git commit -m "chore: add env example"
```

(`.env` resta gitignored.)

---

## Fase 1 — Database

### Task 1.0: Crea database Turso

**Manuale (Salvatore o Paolo da CLI):**

```bash
# Installa CLI Turso se manca: brew install tursodatabase/tap/turso
turso auth login
turso db create toto-dev
turso db show toto-dev --url
turso db tokens create toto-dev
```

Copia URL e token in `.env` locale. Il DB di produzione (`toto-prod`) lo crea Paolo prima del deploy (Task 9.x) e i secret vanno in Vercel.

---

### Task 1.1: Migration SQL iniziale

**Files:**
- Create: `migrations/001_init.sql`

Contenuto: copia integralmente lo schema dalla **sezione 4 del design** (`docs/plans/2026-05-02-toto-design.md`), tutte le 5 tabelle e gli index. Niente seed: lo fa lo script `migrate.ts`.

**Step 1: Commit**

```bash
git add migrations/001_init.sql
git commit -m "feat(db): initial schema migration"
```

---

### Task 1.2: Client Turso

**Files:**
- Create: `src/lib/server/db.ts`

```ts
import { createClient } from '@libsql/client';
import { env } from '$env/dynamic/private';

if (!env.TURSO_DB_URL) throw new Error('TURSO_DB_URL not set');

export const db = createClient({
  url: env.TURSO_DB_URL,
  authToken: env.TURSO_AUTH_TOKEN
});
```

**Step 1: Commit**

```bash
git add src/lib/server/db.ts
git commit -m "feat(db): add Turso client"
```

---

### Task 1.3: Script di migrazione + seed

**Files:**
- Create: `scripts/migrate.ts`

```ts
import { createClient } from '@libsql/client';
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import bcrypt from 'bcryptjs';
import 'dotenv/config';

const url = process.env.TURSO_DB_URL!;
const authToken = process.env.TURSO_AUTH_TOKEN;
const db = createClient({ url, authToken });

async function run() {
  const dir = 'migrations';
  const files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort();
  for (const f of files) {
    const sql = readFileSync(join(dir, f), 'utf8');
    const statements = sql.split(/;\s*$/m).map(s => s.trim()).filter(Boolean);
    for (const stmt of statements) {
      await db.execute(stmt);
    }
    console.log(`✓ ${f}`);
  }

  // Seed utente iniziale (idempotente)
  const email = process.env.INITIAL_USER_EMAIL!;
  const password = process.env.INITIAL_USER_PASSWORD!;
  const existing = await db.execute({
    sql: 'SELECT id FROM users WHERE email = ?',
    args: [email]
  });
  if (existing.rows.length === 0) {
    const hash = await bcrypt.hash(password, 10);
    await db.execute({
      sql: 'INSERT INTO users (email, password_hash, must_change_password) VALUES (?, ?, 1)',
      args: [email, hash]
    });
    console.log(`✓ Seed user ${email}`);
  } else {
    console.log(`= User ${email} already exists, skipping seed`);
  }
}

run().catch(e => { console.error(e); process.exit(1); });
```

Aggiungi dotenv:

```bash
npm install -D dotenv
```

**Step 2: Esegui contro DB dev**

```bash
npm run migrate
```

Atteso: `✓ 001_init.sql` + `✓ Seed user salvatore.damato@circet.it`. Rieseguendolo: `= User ... already exists`.

**Step 3: Commit**

```bash
git add scripts/migrate.ts package.json package-lock.json
git commit -m "feat(db): migration runner with idempotent seed"
```

---

### Task 1.4: Repository layer — TEST FIRST

**Files:**
- Create: `src/lib/server/repositories/comuni.ts`
- Create: `src/lib/server/repositories/comuni.test.ts`

**Step 1: Scrivi i test prima**

```ts
// comuni.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createClient } from '@libsql/client';
import { readFileSync } from 'node:fs';
import { ComuniRepo } from './comuni';

const db = createClient({ url: ':memory:' });
const repo = new ComuniRepo(db);

beforeEach(async () => {
  const sql = readFileSync('migrations/001_init.sql', 'utf8');
  for (const stmt of sql.split(/;\s*$/m).map(s => s.trim()).filter(Boolean)) {
    await db.execute(stmt);
  }
  await db.execute('DELETE FROM comuni');
});

describe('ComuniRepo', () => {
  it('crea un comune con slug auto', async () => {
    const c = await repo.create('Montemurlo');
    expect(c.slug).toBe('montemurlo');
    expect(c.nome).toBe('Montemurlo');
  });

  it('genera slug univoci con suffisso numerico', async () => {
    await repo.create('San Donato');
    const c2 = await repo.create('San Donato');
    expect(c2.slug).toBe('san-donato-2');
  });

  it('lista comuni con conteggio righe e ultimo import', async () => {
    await repo.create('A');
    await repo.create('B');
    const list = await repo.listWithStats();
    expect(list).toHaveLength(2);
    expect(list[0].rowCount).toBe(0);
  });

  it('elimina comune con cascade su rows e imports', async () => {
    const c = await repo.create('Da Eliminare');
    await repo.delete(c.id);
    const list = await repo.listWithStats();
    expect(list).toHaveLength(0);
  });

  it('rinomina senza cambiare slug', async () => {
    const c = await repo.create('Vecchio');
    const r = await repo.rename(c.id, 'Nuovo');
    expect(r.nome).toBe('Nuovo');
    expect(r.slug).toBe('vecchio');
  });
});
```

**Step 2: Verifica fallimento**

```bash
npm test src/lib/server/repositories/comuni.test.ts
```

Atteso: FAIL "Cannot find module './comuni'".

**Step 3: Implementa**

```ts
// comuni.ts
import type { Client } from '@libsql/client';

export interface Comune {
  id: number;
  nome: string;
  slug: string;
  created_at: string;
}

export interface ComuneStats extends Comune {
  rowCount: number;
  lastImportAt: string | null;
}

export class ComuniRepo {
  constructor(private db: Client) {}

  private slugify(s: string): string {
    return s.toLowerCase()
      .normalize('NFD').replace(/[̀-ͯ]/g, '')
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '');
  }

  async create(nome: string): Promise<Comune> {
    const baseSlug = this.slugify(nome);
    let slug = baseSlug;
    let n = 1;
    while (true) {
      const existing = await this.db.execute({
        sql: 'SELECT id FROM comuni WHERE slug = ?',
        args: [slug]
      });
      if (existing.rows.length === 0) break;
      n++;
      slug = `${baseSlug}-${n}`;
    }
    const r = await this.db.execute({
      sql: 'INSERT INTO comuni (nome, slug) VALUES (?, ?) RETURNING *',
      args: [nome, slug]
    });
    return r.rows[0] as unknown as Comune;
  }

  async rename(id: number, nome: string): Promise<Comune> {
    const r = await this.db.execute({
      sql: 'UPDATE comuni SET nome = ? WHERE id = ? RETURNING *',
      args: [nome, id]
    });
    return r.rows[0] as unknown as Comune;
  }

  async delete(id: number): Promise<void> {
    await this.db.execute({ sql: 'DELETE FROM comuni WHERE id = ?', args: [id] });
  }

  async findBySlug(slug: string): Promise<Comune | null> {
    const r = await this.db.execute({
      sql: 'SELECT * FROM comuni WHERE slug = ?',
      args: [slug]
    });
    return (r.rows[0] as unknown as Comune) ?? null;
  }

  async listWithStats(): Promise<ComuneStats[]> {
    const r = await this.db.execute(`
      SELECT c.*,
             (SELECT COUNT(*) FROM rows WHERE comune_id = c.id) AS rowCount,
             (SELECT MAX(uploaded_at) FROM imports WHERE comune_id = c.id) AS lastImportAt
      FROM comuni c
      ORDER BY c.nome
    `);
    return r.rows.map(row => ({
      id: Number(row.id),
      nome: String(row.nome),
      slug: String(row.slug),
      created_at: String(row.created_at),
      rowCount: Number(row.rowCount),
      lastImportAt: row.lastImportAt ? String(row.lastImportAt) : null
    }));
  }
}
```

**Step 4: Verifica passaggio**

```bash
npm test src/lib/server/repositories/comuni.test.ts
```

Atteso: 5 PASS.

**Step 5: Commit**

```bash
git add src/lib/server/repositories/comuni.ts src/lib/server/repositories/comuni.test.ts
git commit -m "feat(db): comuni repository with tests"
```

---

### Task 1.5: Repository `users` — TEST FIRST

**Files:**
- Create: `src/lib/server/repositories/users.ts`
- Create: `src/lib/server/repositories/users.test.ts`

**Step 1: Test**

Test: trova per email, verifica password (bcrypt), aggiorna password (azzera `must_change_password`).

```ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createClient } from '@libsql/client';
import { readFileSync } from 'node:fs';
import bcrypt from 'bcryptjs';
import { UsersRepo } from './users';

const db = createClient({ url: ':memory:' });
const repo = new UsersRepo(db);

beforeEach(async () => {
  const sql = readFileSync('migrations/001_init.sql', 'utf8');
  for (const s of sql.split(/;\s*$/m).map(x => x.trim()).filter(Boolean)) {
    await db.execute(s);
  }
  await db.execute('DELETE FROM users');
  const hash = await bcrypt.hash('admin', 10);
  await db.execute({
    sql: 'INSERT INTO users (email, password_hash, must_change_password) VALUES (?, ?, 1)',
    args: ['s@c.it', hash]
  });
});

describe('UsersRepo', () => {
  it('verify success con password corretta', async () => {
    const u = await repo.verifyCredentials('s@c.it', 'admin');
    expect(u).not.toBeNull();
    expect(u!.email).toBe('s@c.it');
    expect(u!.must_change_password).toBe(1);
  });

  it('verify failure con password sbagliata', async () => {
    const u = await repo.verifyCredentials('s@c.it', 'wrong');
    expect(u).toBeNull();
  });

  it('changePassword aggiorna hash e azzera must_change_password', async () => {
    const u = await repo.verifyCredentials('s@c.it', 'admin');
    await repo.changePassword(u!.id, 'newpass');
    const old = await repo.verifyCredentials('s@c.it', 'admin');
    expect(old).toBeNull();
    const next = await repo.verifyCredentials('s@c.it', 'newpass');
    expect(next!.must_change_password).toBe(0);
  });
});
```

**Step 2: Implementa**

```ts
import type { Client } from '@libsql/client';
import bcrypt from 'bcryptjs';

export interface User {
  id: number;
  email: string;
  password_hash: string;
  must_change_password: number;
  created_at: string;
}

export class UsersRepo {
  constructor(private db: Client) {}

  async findByEmail(email: string): Promise<User | null> {
    const r = await this.db.execute({
      sql: 'SELECT * FROM users WHERE email = ?',
      args: [email]
    });
    return (r.rows[0] as unknown as User) ?? null;
  }

  async findById(id: number): Promise<User | null> {
    const r = await this.db.execute({
      sql: 'SELECT * FROM users WHERE id = ?',
      args: [id]
    });
    return (r.rows[0] as unknown as User) ?? null;
  }

  async verifyCredentials(email: string, password: string): Promise<User | null> {
    const u = await this.findByEmail(email);
    if (!u) return null;
    const ok = await bcrypt.compare(password, u.password_hash);
    return ok ? u : null;
  }

  async changePassword(userId: number, newPassword: string): Promise<void> {
    const hash = await bcrypt.hash(newPassword, 10);
    await this.db.execute({
      sql: 'UPDATE users SET password_hash = ?, must_change_password = 0 WHERE id = ?',
      args: [hash, userId]
    });
  }
}
```

**Step 3: Test passa, commit**

```bash
npm test src/lib/server/repositories/users.test.ts
git add src/lib/server/repositories/users.ts src/lib/server/repositories/users.test.ts
git commit -m "feat(db): users repository with tests"
```

---

### Task 1.6: Repository `sessions` — TEST FIRST

**Files:**
- Create: `src/lib/server/repositories/sessions.ts`, `.test.ts`

Test:
- `create(userId, ttl)` ritorna token random ≥32 char URL-safe + `expires_at` futuro
- `find(token)` ritorna `{user, session}` se valida, `null` se scaduta
- `delete(token)` invalida la sessione
- `deleteAllForUser(userId)` invalida tutte le sessioni di un utente

Implementazione: usa `crypto.randomBytes(32).toString('base64url')` per token.

Commit: `feat(db): sessions repository with tests`.

---

### Task 1.7: Repository `imports` — TEST FIRST

Test:
- `create(comuneId, filename, userId, rowCount)` ritorna nuova entry
- `latestForComune(comuneId)` ritorna ultimo import o null

Commit: `feat(db): imports repository with tests`.

---

### Task 1.8: Repository `rows` — TEST FIRST

**Files:**
- Create: `src/lib/server/repositories/rows.ts`, `.test.ts`

Metodi richiesti:

```ts
class RowsRepo {
  async deleteByComune(comuneId: number): Promise<number>;
  async insertBatch(comuneId: number, importId: number, rows: RowData[]): Promise<void>; // batch da 500
  async countEditedByComune(comuneId: number): Promise<number>;
  async list(opts: {
    comuneId: number;
    limit: number;
    offset: number;
    search?: string;
    filters?: Partial<Record<'assistente' | 'squadra' | 'fm' | 'n_foglio', string>>;
  }): Promise<{ total: number; rows: RowRecord[] }>;
  async findById(id: number): Promise<RowRecord | null>;
  async update(id: number, patch: Partial<RowRecord>): Promise<void>; // setta edited=1, edited_at=now
  async distinctValues(comuneId: number, column: 'assistente' | 'squadra' | 'fm' | 'n_foglio'): Promise<string[]>;
}
```

Test (almeno 6):
1. `deleteByComune` cancella solo righe del comune passato
2. `insertBatch` inserisce 1500 righe in 3 chunk e tutte sono presenti
3. `list` con paginazione ritorna `total` corretto e `rows.length === limit`
4. `list` con `search` filtra su colonne testuali principali (LIKE %term%)
5. `list` con `filters.assistente` filtra correttamente
6. `update` marca `edited=1` e popola `edited_at`

Implementazione: usa `db.batch([...])` di libsql per efficienza, `?` placeholder per ogni colonna.

Commit: `feat(db): rows repository with tests`.

---

## Fase 2 — Excel parsing

### Task 2.1: Modulo `columns` — mapping header

**Files:**
- Create: `src/lib/server/columns.ts`, `.test.ts`

**Step 1: Definisci mapping**

```ts
// columns.ts
export const COLUMN_MAP: Record<string, string> = {
  'ID Ord.': 'id_ord',
  'F/M': 'fm',
  'N° Foglio': 'n_foglio',
  'Riga': 'riga',
  'An.': 'an',
  'A.': 'a_col',
  'T.': 't_col',
  'C.': 'c_col',
  'Desc. Trec.': 'desc_trec',
  'Cod. Prestazione': 'cod_prestazione',
  'Foglio / Prestazione': 'foglio_prestazione',
  'Desc. Armadio': 'desc_armadio',
  'Desc. Secondaria': 'desc_secondaria',
  'U.M.': 'um',
  'Quantità': 'quantita',
  'C.A.': 'ca',
  'Valo. Unit.': 'valo_unit',
  'Valore Record': 'valore_record',
  'Tot. Punti': 'tot_punti',
  'Tipo V.': 'tipo_v',
  '% Man.': 'perc_man',
  '% Mat.': 'perc_mat',
  'Squadra': 'squadra',
  'Assistente': 'assistente',
  'Dipen.': 'dipen',
  'Data Prod.': 'data_prod',
  'Prod.': 'prod',
  'H_Cl.': 'h_cl',
  'Estevoce': 'estevoce',
  'Keylogol': 'keylogol',
  'Building': 'building',
  'Da Rete': 'da_rete',
  'Desc. Da Rete': 'desc_da_rete',
  'A Rete': 'a_rete',
  'Desc. A Rete': 'desc_a_rete',
  'Larghezza': 'larghezza',
  'Lunghezza': 'lunghezza',
  'Profondità': 'profondita',
  'Note': 'note',
  'Discrim.': 'discrim',
  'Bonifica': 'bonifica',
  'Seriale': 'seriale',
  'Val. Subapp.': 'val_subapp',
  'Utente XME': 'utente_xme',
  'Ultimo Update': 'ultimo_update',
  'ID Lavori': 'id_lavori',
  'ID Fogli': 'id_fogli',
  'ID Misure': 'id_misure',
  'Val. Sconto': 'val_sconto',
  'Codice SAP': 'codice_sap',
  'D.Inizio': 'd_inizio',
  'D.Fine': 'd_fine',
  'D.Chiusura': 'd_chiusura',
  'Desc. Prest. 2': 'desc_prest_2'
};

export const EXPECTED_HEADERS = Object.keys(COLUMN_MAP);
export const DB_COLUMNS = Object.values(COLUMN_MAP);

export const VISIBLE_DEFAULT = [
  'desc_trec', 'cod_prestazione', 'foglio_prestazione', 'desc_armadio',
  'desc_secondaria', 'um', 'quantita', 'squadra', 'assistente',
  'data_prod', 'note', 'ultimo_update'
] as const;
```

**Step 2: Test**

```ts
import { describe, it, expect } from 'vitest';
import { COLUMN_MAP, EXPECTED_HEADERS, VISIBLE_DEFAULT } from './columns';

describe('columns', () => {
  it('ha 54 colonne', () => {
    expect(EXPECTED_HEADERS).toHaveLength(54);
  });
  it('VISIBLE_DEFAULT contiene esattamente 12 colonne tutte presenti nel mapping', () => {
    expect(VISIBLE_DEFAULT).toHaveLength(12);
    for (const col of VISIBLE_DEFAULT) {
      expect(Object.values(COLUMN_MAP)).toContain(col);
    }
  });
});
```

**Step 3: Commit**

```bash
npm test src/lib/server/columns.test.ts
git add src/lib/server/columns.ts src/lib/server/columns.test.ts
git commit -m "feat(excel): column mapping Excel↔DB"
```

---

### Task 2.2: Parser Excel — TEST FIRST

**Files:**
- Create: `src/lib/server/excel.ts`, `.test.ts`

**Step 1: Test usando il file reale**

```ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { parseEstrazione } from './excel';

const buf = readFileSync('docs/Estrazioen totale.xlsx');

describe('parseEstrazione', () => {
  it('estrae righe dal sheet Sheet ignorando Foglio1', () => {
    const result = parseEstrazione(buf);
    expect(result.rows.length).toBeGreaterThan(5000);
    expect(result.rows.length).toBeLessThan(5300);
  });

  it('ogni riga ha tutte le 54 chiavi snake_case', () => {
    const { rows } = parseEstrazione(buf);
    const keys = Object.keys(rows[0]);
    expect(keys).toHaveLength(54);
    expect(keys).toContain('id_misure');
    expect(keys).toContain('desc_trec');
  });

  it('converte Quantità a number', () => {
    const { rows } = parseEstrazione(buf);
    const sample = rows.find(r => r.quantita !== null && r.quantita !== 0);
    expect(typeof sample!.quantita).toBe('number');
  });

  it('converte date a stringa ISO', () => {
    const { rows } = parseEstrazione(buf);
    const sample = rows.find(r => r.data_prod);
    expect(sample!.data_prod).toMatch(/^\d{4}-\d{2}-\d{2}/);
  });

  it('throw se mancano headers attesi', () => {
    // costruisci un buffer Excel con headers errati e verifica throw
    // (usa xlsx.utils per generarlo al volo)
  });
});
```

**Step 2: Implementazione**

```ts
import * as XLSX from 'xlsx';
import { COLUMN_MAP, EXPECTED_HEADERS } from './columns';

export interface ParsedRow {
  [key: string]: string | number | null;
}

export interface ParseResult {
  rows: ParsedRow[];
  headers: string[];
}

export function parseEstrazione(buffer: ArrayBuffer | Buffer): ParseResult {
  const wb = XLSX.read(buffer, { type: 'buffer', cellDates: true });
  if (!wb.SheetNames.includes('Sheet')) {
    throw new Error('Foglio "Sheet" non trovato nel file Excel');
  }
  const ws = wb.Sheets['Sheet'];
  const rawRows: any[][] = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true, defval: null });

  if (rawRows.length === 0) throw new Error('Foglio vuoto');

  const headers = (rawRows[0] as any[]).map(h => String(h ?? '').trim());
  const missing = EXPECTED_HEADERS.filter(h => !headers.includes(h));
  if (missing.length > 0) {
    throw new Error(`Colonne mancanti: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? '…' : ''}`);
  }

  const headerToIndex = new Map<string, number>();
  headers.forEach((h, i) => headerToIndex.set(h, i));

  const rows: ParsedRow[] = [];
  for (let r = 1; r < rawRows.length; r++) {
    const row = rawRows[r];
    if (!row || row.every(c => c === null || c === '')) continue;
    const obj: ParsedRow = {};
    for (const [excelHeader, dbCol] of Object.entries(COLUMN_MAP)) {
      const idx = headerToIndex.get(excelHeader);
      const v = idx !== undefined ? row[idx] : null;
      obj[dbCol] = normalizeValue(v);
    }
    rows.push(obj);
  }
  return { rows, headers };
}

function normalizeValue(v: unknown): string | number | null {
  if (v === null || v === undefined || v === '') return null;
  if (v instanceof Date) return v.toISOString();
  if (typeof v === 'string') {
    const trimmed = v.trim();
    return trimmed === '' ? null : trimmed;
  }
  return v as number;
}
```

**Step 3: Test passa, commit**

```bash
npm test src/lib/server/excel.test.ts
git add src/lib/server/excel.ts src/lib/server/excel.test.ts
git commit -m "feat(excel): parse Estrazione totale into 54-col records"
```

---

## Fase 3 — Auth

### Task 3.1: Helpers auth (sessione + cookie)

**Files:**
- Create: `src/lib/server/auth.ts`

```ts
import type { Cookies } from '@sveltejs/kit';
import { db } from './db';
import { SessionsRepo } from './repositories/sessions';
import { UsersRepo } from './repositories/users';

const SESSION_COOKIE = 'toto_session';
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30; // 30 days

const sessionsRepo = new SessionsRepo(db);
const usersRepo = new UsersRepo(db);

export async function login(cookies: Cookies, userId: number): Promise<void> {
  const token = await sessionsRepo.create(userId, SESSION_TTL_SECONDS);
  cookies.set(SESSION_COOKIE, token, {
    path: '/',
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: SESSION_TTL_SECONDS
  });
}

export async function logout(cookies: Cookies): Promise<void> {
  const token = cookies.get(SESSION_COOKIE);
  if (token) await sessionsRepo.delete(token);
  cookies.delete(SESSION_COOKIE, { path: '/' });
}

export async function getCurrentUser(cookies: Cookies) {
  const token = cookies.get(SESSION_COOKIE);
  if (!token) return null;
  const found = await sessionsRepo.find(token);
  if (!found) return null;
  return found.user;
}

export { usersRepo, sessionsRepo };
```

Commit: `feat(auth): session helpers`.

---

### Task 3.2: Hook server (middleware auth)

**Files:**
- Create: `src/hooks.server.ts`
- Modify: `src/app.d.ts`

```ts
// hooks.server.ts
import { redirect, type Handle } from '@sveltejs/kit';
import { getCurrentUser } from '$lib/server/auth';

const PUBLIC = ['/login'];

export const handle: Handle = async ({ event, resolve }) => {
  const user = await getCurrentUser(event.cookies);
  event.locals.user = user;

  const path = event.url.pathname;
  const isPublic = PUBLIC.some(p => path === p || path.startsWith(p + '/'));

  if (!user && !isPublic) {
    throw redirect(303, '/login');
  }
  if (user && user.must_change_password === 1 && path !== '/cambia-password' && path !== '/logout') {
    throw redirect(303, '/cambia-password');
  }

  return resolve(event);
};
```

```ts
// app.d.ts
declare global {
  namespace App {
    interface Locals {
      user: { id: number; email: string; must_change_password: number } | null;
    }
  }
}
export {};
```

Commit: `feat(auth): middleware enforcing login`.

---

### Task 3.3: Route `/login`

**Files:**
- Create: `src/routes/login/+page.svelte`, `+page.server.ts`

`+page.server.ts`:

```ts
import { fail, redirect, type Actions } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { UsersRepo } from '$lib/server/repositories/users';
import { login } from '$lib/server/auth';

const usersRepo = new UsersRepo(db);

export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData();
    const email = String(data.get('email') ?? '').trim();
    const password = String(data.get('password') ?? '');
    if (!email || !password) return fail(400, { error: 'Email e password obbligatorie', email });
    const user = await usersRepo.verifyCredentials(email, password);
    if (!user) return fail(401, { error: 'Credenziali non valide', email });
    await login(cookies, user.id);
    throw redirect(303, '/');
  }
};
```

`+page.svelte`: form essenziale (email + password + submit), mostra errori da `form?.error`.

Commit: `feat(auth): login page`.

---

### Task 3.4: Route `/cambia-password`

**Files:**
- Create: `src/routes/cambia-password/+page.svelte`, `+page.server.ts`

Form con: vecchia password + nuova + conferma. Action verifica vecchia, chiama `usersRepo.changePassword`, invalida tutte le altre sessioni di quell'utente, redirect a `/`.

Commit: `feat(auth): change password page`.

---

### Task 3.5: Route `/logout`

**Files:**
- Create: `src/routes/logout/+server.ts` (o `+page.server.ts` con action)

POST → `await logout(cookies)` → redirect `/login`.

Commit: `feat(auth): logout route`.

---

### Task 3.6: Layout root con header utente

**Files:**
- Modify: `src/routes/+layout.svelte`, `+layout.server.ts`

`+layout.server.ts`:

```ts
import { db } from '$lib/server/db';
import { ComuniRepo } from '$lib/server/repositories/comuni';

const comuniRepo = new ComuniRepo(db);

export const load = async ({ locals }) => {
  if (!locals.user) return { user: null, comuni: [] };
  const comuni = await comuniRepo.listWithStats();
  return { user: locals.user, comuni };
};
```

`+layout.svelte`: header con email utente + dropdown comuni + link `Comuni` + bottone logout. Mostra solo se loggato.

Commit: `feat(ui): root layout with user header and comune switcher`.

---

## Fase 4 — Comuni CRUD

### Task 4.1: Pagina `/comuni`

**Files:**
- Create: `src/routes/comuni/+page.svelte`, `+page.server.ts`

`+page.server.ts`: `load` ritorna `comuniRepo.listWithStats()`. `actions`:
- `create`: nome → `comuniRepo.create`, fail 400 se vuoto o duplicato.
- `rename`: id + nome → `comuniRepo.rename`.
- `delete`: id → `comuniRepo.delete`.

`+page.svelte`: tabella comuni (nome, righe, ultimo import, pulsanti Apri / Rinomina / Elimina). Form inline per crearne uno nuovo. Conferma JS su delete (`confirm("Eliminerai N righe…")`).

Commit: `feat(comuni): CRUD page`.

---

### Task 4.2: Layout `/comune/[slug]/+layout.server.ts`

**Files:**
- Create: `src/routes/comune/[slug]/+layout.server.ts`

```ts
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { ComuniRepo } from '$lib/server/repositories/comuni';

const comuniRepo = new ComuniRepo(db);

export const load = async ({ params }) => {
  const c = await comuniRepo.findBySlug(params.slug);
  if (!c) throw error(404, 'Comune non trovato');
  return { comune: c };
};
```

Commit: `feat(comuni): scoped layout for slug routes`.

---

### Task 4.3: Dashboard `/comune/[slug]/+page.svelte`

Mostra: nome comune, info ultimo import (data, file, n.righe), bottoni "Importa Excel" e "Visualizza dati".

Commit: `feat(comuni): per-comune dashboard`.

---

### Task 4.4: Dashboard globale `/`

**Files:**
- Modify: `src/routes/+page.svelte`, `+page.server.ts`

Lista i comuni con card cliccabili. Se zero comuni: messaggio "Nessun comune configurato. Vai a /comuni per crearne uno".

Commit: `feat(ui): home dashboard`.

---

## Fase 5 — Import Excel

### Task 5.1: Servizio import (logica server) — TEST FIRST

**Files:**
- Create: `src/lib/server/importService.ts`, `.test.ts`

```ts
class ImportService {
  async run(opts: {
    comuneId: number;
    userId: number;
    filename: string;
    fileBuffer: Buffer;
    confirmDestroyEdits?: boolean;
  }): Promise<{ rowCount: number; importId: number } | { needsConfirm: number }>;
}
```

Comportamento:
1. Parse Excel.
2. Conta `editedRows = rowsRepo.countEditedByComune(comuneId)`.
3. Se `editedRows > 0 && !confirmDestroyEdits` → ritorna `{ needsConfirm: editedRows }` senza toccare il DB.
4. Altrimenti: in transazione → delete vecchie righe → crea import → batch insert nuove righe.
5. Ritorna `{ rowCount, importId }`.

Test: 5+ casi, in particolare il branch `needsConfirm`.

Commit: `feat(import): import service with edit-confirm guard`.

---

### Task 5.2: Pagina `/comune/[slug]/import`

**Files:**
- Create: `src/routes/comune/[slug]/import/+page.svelte`, `+page.server.ts`

Form `enctype="multipart/form-data"` con input `file` accept `.xlsx` + checkbox nascosto `confirm_destroy_edits` (impostato a true se l'utente ha confermato l'alert).

Action:
- Legge `request.formData()`, ottiene `File`, converte in Buffer.
- Chiama `ImportService.run`.
- Se `needsConfirm` → `fail(409, { needsConfirm: N })`.
- Altrimenti redirect a `/comune/[slug]/data`.

UI: drag&drop semplice (input `file` stilizzato), riquadro che mostra info ultimo import, riquadro errori, dialog di conferma per perdita modifiche.

Commit: `feat(import): upload page`.

---

## Fase 6 — Tabella dati

### Task 6.1: Endpoint API server-side per la tabella

**Files:**
- Create: `src/routes/comune/[slug]/data/+page.server.ts`

`load` accetta query params: `page`, `pageSize`, `q` (search), `assistente`, `squadra`, `fm`, `n_foglio`. Chiama `rowsRepo.list(...)`. Ritorna `{ rows, total, page, pageSize, distinct: { assistente: [...], ... } }`.

Action `update`: id + patch JSON → `rowsRepo.update`.

Commit: `feat(data): page loader and update action`.

---

### Task 6.2: Componente `DataTable.svelte`

**Files:**
- Create: `src/lib/components/DataTable.svelte`

Props: `rows`, `total`, `page`, `pageSize`, `visibleColumns`, `onEdit(rowId, patch)`. Renderizza:
- Header con nomi colonne (italiani) + pulsante "Colonne" che apre `ColumnPicker`.
- Tbody: righe con `F/M=T` evidenziate (sfondo grigio, font bold), righe `M` normali con badge `modificata` se `edited=1`.
- Edit inline (su `quantita`, `note`, `desc_secondaria`): input che on blur invia form action `?/update`.
- Click su altra cella → emette evento `select(row)` per aprire drawer.

Commit: `feat(data): DataTable component`.

---

### Task 6.3: Componenti `ColumnPicker` e `RowDrawer`

**Files:**
- Create: `src/lib/components/ColumnPicker.svelte`, `RowDrawer.svelte`

`ColumnPicker`: popover con checkbox per ogni colonna; salva stato in `localStorage` (`toto.visibleColumns`).

`RowDrawer`: pannello laterale (CSS `position: fixed; right: 0`) con form per **tutte e 54** le colonne, pulsante "Salva" che chiama action.

Commit: `feat(data): column picker and row drawer`.

---

### Task 6.4: Pagina `/comune/[slug]/data/+page.svelte`

Compone: filtri (input search debounced 300ms + select Assistente/Squadra/F/M/N° Foglio) + `DataTable` + `RowDrawer` + paginazione (Prev / Page X di Y / Next + select pageSize). Sincronizza tutti i parametri in URL (`goto(?q=...&page=...)`).

Commit: `feat(data): full data view page`.

---

## Fase 7 — Polish

### Task 7.1: Toast notifications

**Files:**
- Create: `src/lib/components/Toast.svelte`, store `src/lib/stores/toasts.ts`

Store `toasts` (writable di array di `{id, kind, message}`). Componente in `+layout.svelte` che li renderizza in basso a destra. Helper `toast.success(msg)`, `toast.error(msg)`. Usato dopo import, save, delete.

Commit: `feat(ui): toast notifications`.

---

### Task 7.2: Stile CSS minimal e RESPONSIVE

Crea `src/lib/styles/app.css` importato da `+layout.svelte`. Stile pulito, neutro: tabella zebrata, hover row, header sticky, drawer animato, input/button base. Niente Tailwind (YAGNI per scala progetto).

**Responsive (vedi sezione 6.bis del design):** breakpoint a 640px / 1024px / 1280px.

- **Header:** su mobile (<640px) menu hamburger che apre pannello collassabile con switcher comune + link Comuni + cambia password + logout. Su tablet/desktop barra orizzontale.
- **DataTable:** su mobile (<640px) layout "card-stack" — ogni riga è una card verticale con label+valore per `Desc. Trec.`, `Quantità`, `Assistente`, `Data Prod.`; tap apre il drawer. Su tablet/desktop tabella tradizionale con scroll-x se serve.
- **RowDrawer:** su desktop pannello 480px sulla destra; su mobile fullscreen slide-up.
- **Form/input:** full-width su mobile, target tap minimo 44×44px (padding 12px verticali per `<button>` e `<input>`).
- **Filtri:** su mobile chip orizzontali scrollabili (`overflow-x: auto`); su desktop select/dropdown.
- **Paginazione:** su mobile solo Prev / "X di Y" / Next; su desktop anche selettore pageSize.

Per la DataTable la modalità (table vs card-stack) si decide via CSS media query, NON via JS — un solo template Svelte con due rendering CSS-condizionali (`<table>` nascosto sotto 640px, `<ul>` nascosto sopra). Questo evita layout shift e mantiene tutto SSR-friendly.

Test manuale di verifica: aprire DevTools → device toolbar → testare iPhone 13 / iPad / desktop 1440. Tutte le route principali (login, dashboard, comuni, dati) devono essere usabili senza scroll orizzontale spurio.

Commit: `style: base CSS with responsive layout`.

---

### Task 7.3: README

**Files:**
- Create: `README.md`

Contenuto:
- Descrizione progetto (1 paragrafo)
- Setup locale (clone, `npm install`, crea Turso, `.env`, `npm run migrate`, `npm run dev`)
- Variabili ambiente
- Deploy su Vercel (link a docs Vercel + env da impostare)
- Struttura cartelle (riferimento al design)

Commit: `docs: README`.

---

## Fase 8 — Smoke test E2E (opzionale ma consigliato)

### Task 8.1: Setup Playwright

```bash
npm init playwright@latest -- --quiet --browser=chromium --no-examples --ts
```

Configura `playwright.config.ts` per `webServer: 'npm run dev'`.

Commit: `chore: setup playwright`.

---

### Task 8.2: E2E happy path

**Files:**
- Create: `e2e/happy-path.spec.ts`

Scenario:
1. Vai su `/`, redirect a `/login`.
2. Login con `salvatore.damato@circet.it` / `admin`.
3. Redirect a `/cambia-password`. Cambia in `test1234`.
4. Vai a `/comuni`, crea "Montemurlo".
5. Vai a `/comune/montemurlo/import`, upload `docs/Estrazioen totale.xlsx`.
6. Vai a `/comune/montemurlo/data`, verifica che ci sono >5000 righe (dal contatore paginazione).
7. Modifica `note` su una riga, verifica badge "modificata".
8. Logout.

Run: `npm run e2e`.

Aggiungi un secondo run con viewport mobile (`devices['iPhone 13']`) per verificare il flusso login → dashboard → tabella in modalità card-stack.

Commit: `test: e2e happy path (desktop + mobile viewport)`.

---

## Fase 9 — Deploy Vercel

### Task 9.1: Crea DB Turso production

```bash
turso db create toto-prod
turso db show toto-prod --url
turso db tokens create toto-prod
```

Salva valori per Vercel env.

---

### Task 9.2: Esegui migrazioni su prod

```bash
TURSO_DB_URL=<prod-url> TURSO_AUTH_TOKEN=<prod-token> \
  INITIAL_USER_EMAIL=salvatore.damato@circet.it INITIAL_USER_PASSWORD=admin \
  npm run migrate
```

Verifica seed utente.

---

### Task 9.3: Push su GitHub privato

```bash
gh repo create toto --private --source=. --remote=origin --push
```

Se `gh` non disponibile: crea repo manualmente e:

```bash
git remote add origin git@github.com:<user>/toto.git
git branch -M main
git push -u origin main
```

⚠️ **Conferma con l'utente prima di creare il repo remoto** (azione visibile su GitHub).

---

### Task 9.4: Deploy su Vercel

**Manuale via dashboard Vercel:**
1. Import repo `toto`.
2. Framework auto-detect: SvelteKit.
3. Aggiungi env vars (production):
   - `TURSO_DB_URL` = `<prod-url>`
   - `TURSO_AUTH_TOKEN` = `<prod-token>`
   - `COOKIE_SECRET` = `openssl rand -base64 32`
   - `INITIAL_USER_EMAIL` = `salvatore.damato@circet.it`
   - `INITIAL_USER_PASSWORD` = `admin`
4. Deploy.
5. Verifica URL e login funzionante.

Commit (in repo): `docs: deployment notes` (aggiungi sezione Deploy nel README con link al progetto Vercel).

---

## Criteri di accettazione finali

- [ ] Login con `salvatore.damato@circet.it` / `admin` funziona, redirect a cambio password al primo accesso.
- [ ] Cambio password funziona e persiste.
- [ ] Creazione/rinomina/eliminazione comune funziona.
- [ ] Upload Excel ≥5k righe completa in <30s su Vercel.
- [ ] Dopo upload, `/comune/[slug]/data` mostra le righe paginate con le 12 colonne di default.
- [ ] Selettore colonne mostra/nasconde le 54 colonne, preferenza ricordata.
- [ ] Filtri Assistente / Squadra / F/M / N° Foglio + ricerca testuale funzionano e si combinano.
- [ ] Edit inline su `Quantità`/`Note`/`Desc. Secondaria` salva e marca riga come `modificata`.
- [ ] Drawer dettaglio mostra e salva tutte le 54 colonne.
- [ ] Reimport con modifiche manuali presenti chiede conferma.
- [ ] App online su URL Vercel, accessibile da Salvatore.
- [ ] Tutti i test (`npm test`) passano.

---

## Quando questo piano è completato

Aspettiamo che Salvatore usi l'app per qualche giorno e raccolga feedback su:
- Quali colonne mancano nella vista predefinita?
- Quali filtri/raggruppamenti aggiuntivi servono?
- Schema/output finale da generare partendo da questi dati (la fase successiva del progetto).
