# Toto — Design

**Data:** 2026-05-02
**Cliente finale:** Salvatore D'Amato (Circet)
**Stato:** approvato, pronto per fase di pianificazione implementativa

## 1. Obiettivo

Web app personale che permette a Salvatore di:

1. Gestire una lista di **comuni** in cui lavora (3-4 attualmente, estendibile).
2. Per ciascun comune, caricare periodicamente un file Excel di "Estrazione totale" (export grezzo dal sistema XME/CIRCET).
3. Visualizzare i dati di ogni comune in una tabella editabile (paginazione, filtri, ricerca).
4. Modificare manualmente le righe (con tracciamento delle modifiche).
5. (Fase futura) Generare schemi/output a partire dai dati — non in questo MVP.

Ogni nuovo import per un comune **sovrascrive completamente** i dati di quel comune (gli altri restano intatti).

## 2. Stack

| Layer | Scelta | Motivazione |
|---|---|---|
| Framework | **SvelteKit 2** (Svelte 5) | DX semplice, form actions native, ottimo deploy Vercel |
| Hosting | **Vercel Hobby** | Gratuito, zero config per SvelteKit |
| Database | **Turso (SQLite distribuito)** | Free tier 9 GB / 1B row reads mese, SQL completo, edge-friendly |
| Excel parsing | **SheetJS (`xlsx`)** lato server | Parser collaudato, gestisce datetime, formule |
| Auth | Custom session-based (cookie httpOnly) | Single-user, niente bisogno di Auth.js / Lucia |
| Hash password | **bcrypt** (`bcryptjs`) | Standard, no native deps su Vercel |

## 3. Autenticazione

- **Utente unico** precaricato via seed: `salvatore.damato@circet.it` / password iniziale `admin` (bcrypt) con flag `must_change_password=1`.
- **Login**: form email + password. Se `must_change_password=1` → redirect forzato a `/cambia-password`.
- **Sessione**: token random salvato in tabella `sessions`, cookie `httpOnly` `Secure` `SameSite=Lax`, durata 30 giorni rolling.
- **Cambio password**: form con vecchia + nuova + conferma. Invalida le altre sessioni.
- **Hook SvelteKit** (`hooks.server.ts`): tutte le route protette tranne `/login`.
- Tabella `users` predisposta multi-utente per estensione futura — non costruiamo UI multi-utente ora.

## 4. Schema database

```sql
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  must_change_password INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE sessions (
  id TEXT PRIMARY KEY,                    -- token random URL-safe
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  expires_at TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_sessions_user ON sessions(user_id);

CREATE TABLE comuni (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  nome TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,              -- URL-safe (es. "montemurlo")
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE imports (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  comune_id INTEGER NOT NULL REFERENCES comuni(id) ON DELETE CASCADE,
  filename TEXT NOT NULL,
  uploaded_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  uploaded_by INTEGER NOT NULL REFERENCES users(id),
  row_count INTEGER NOT NULL
);
CREATE INDEX idx_imports_comune ON imports(comune_id);

-- Tabella dati: tutte le 54 colonne dell'Excel, snake_case
CREATE TABLE rows (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  comune_id INTEGER NOT NULL REFERENCES comuni(id) ON DELETE CASCADE,
  import_id INTEGER NOT NULL REFERENCES imports(id) ON DELETE CASCADE,

  -- Colonne dall'Excel (54)
  id_ord INTEGER,
  fm TEXT,                    -- F/M ('T' = totale foglio, 'M' = misura)
  n_foglio TEXT,
  riga TEXT,
  an TEXT,
  a_col TEXT,                 -- "A."
  t_col TEXT,                 -- "T."
  c_col TEXT,                 -- "C."
  desc_trec TEXT,
  cod_prestazione TEXT,
  foglio_prestazione TEXT,
  desc_armadio TEXT,
  desc_secondaria TEXT,
  um TEXT,
  quantita REAL,
  ca TEXT,
  valo_unit REAL,
  valore_record REAL,
  tot_punti TEXT,
  tipo_v TEXT,
  perc_man REAL,
  perc_mat REAL,
  squadra TEXT,
  assistente TEXT,
  dipen TEXT,
  data_prod TEXT,
  prod TEXT,
  h_cl TEXT,
  estevoce TEXT,
  keylogol INTEGER,
  building TEXT,
  da_rete INTEGER,
  desc_da_rete TEXT,
  a_rete INTEGER,
  desc_a_rete TEXT,
  larghezza REAL,
  lunghezza REAL,
  profondita REAL,
  note TEXT,
  discrim TEXT,
  bonifica TEXT,
  seriale TEXT,
  val_subapp REAL,
  utente_xme TEXT,
  ultimo_update TEXT,
  id_lavori INTEGER,
  id_fogli INTEGER,
  id_misure INTEGER,
  val_sconto REAL,
  codice_sap TEXT,
  d_inizio TEXT,
  d_fine TEXT,
  d_chiusura TEXT,
  desc_prest_2 TEXT,

  -- Audit
  edited INTEGER NOT NULL DEFAULT 0,
  edited_at TEXT
);

CREATE INDEX idx_rows_comune ON rows(comune_id);
CREATE INDEX idx_rows_import ON rows(import_id);
CREATE INDEX idx_rows_id_misure ON rows(id_misure);
CREATE INDEX idx_rows_id_fogli ON rows(id_fogli);
CREATE INDEX idx_rows_assistente ON rows(assistente);
CREATE INDEX idx_rows_n_foglio ON rows(n_foglio);
CREATE INDEX idx_rows_fm ON rows(fm);
```

Le date (`data_prod`, `ultimo_update`, `d_inizio`, `d_fine`, `d_chiusura`) salvate come ISO 8601 string (`TEXT`), gestite a livello applicativo. Nullable ammessi: SQLite non vincola type strict.

## 5. Flusso di import (per comune)

1. Salvatore va su `/comune/[slug]/import`.
2. Form drag&drop file `.xlsx`.
3. Server (form action SvelteKit):
   1. Salva temporaneamente in memoria.
   2. Parse `Sheet` con SheetJS — **ignora `Foglio1`**.
   3. Verifica che le 54 intestazioni attese siano presenti (validazione).
   4. Conta righe con modifiche manuali (`SELECT COUNT(*) FROM rows WHERE comune_id=? AND edited=1`).
   5. Se >0 e non ha confermato: ritorna form di conferma "stai per perdere N modifiche manuali".
   6. Transazione:
      - `DELETE FROM rows WHERE comune_id=?`
      - `INSERT INTO imports (comune_id, filename, uploaded_by, row_count) VALUES (...)`
      - Batch `INSERT INTO rows ...` (chunk di 500 righe via prepared statement)
   7. Commit.
4. Toast esito + redirect a `/comune/[slug]/data`.

Tipo riga (`F/M`):
- `T` (totale foglio) → importate ma renderizzate diversamente in tabella (header collassabile del gruppo "foglio").
- `M` (misura) → righe figlie modificabili.

## 6. UI / Route

| Route | Scopo |
|---|---|
| `/login` | Form email + password |
| `/cambia-password` | Cambio password (forzato al primo login) |
| `/logout` | POST → invalida sessione |
| `/` | Dashboard globale: card per ogni comune con ultimo import (data, righe), bottone "Apri" |
| `/comuni` | CRUD comuni (lista, crea, rinomina, elimina con conferma) |
| `/comune/[slug]` | Dashboard del comune (info ultimo import + bottoni "Importa" / "Visualizza dati") |
| `/comune/[slug]/import` | Upload Excel |
| `/comune/[slug]/data` | Tabella dati editabile |

**Header globale:** dropdown "Comune attivo" sempre visibile (cambia comune = cambia URL), link a `/`, `/comuni`, profilo (logout / cambia password).

**Tabella dati `/comune/[slug]/data`:**
- 12 colonne visibili di default: `Desc. Trec.`, `Cod. Prestazione`, `Foglio / Prestazione`, `Desc. Armadio`, `Desc. Secondaria`, `U.M.`, `Quantità`, `Squadra`, `Assistente`, `Data Prod.`, `Note`, `Ultimo Update`.
- Selettore "mostra/nascondi colonne" (popover) con tutte le 54 disponibili. Preferenze salvate in `localStorage`.
- Paginazione (50 / 100 / 200 per pagina), default 100.
- Ricerca testuale globale (debounce 300ms, query LIKE su colonne testuali principali).
- Filtri dropdown: `Assistente`, `Squadra`, `F/M`, `N° Foglio`.
- Edit inline su `Quantità`, `Note`, `Desc. Secondaria` (campi più frequenti).
- Click su riga → pannello laterale (drawer) con tutti i 54 campi modificabili.
- Salvataggio via form action SvelteKit. Marca `edited=1` + `edited_at=CURRENT_TIMESTAMP`. Badge "modificata" sulla riga.
- Righe `T` (totali foglio) renderizzate come header del gruppo (sfondo grigio, font bold, espandi/collassa).

**Gestione comuni `/comuni`:**
- Lista comuni con n.righe + data ultimo import.
- Form inline per crearne uno nuovo (nome → slug auto).
- Rinomina (nome editabile, slug invariato per non rompere URL).
- Elimina con dialog di conferma: "Eliminerai anche N righe e M import per [nome]. Sicuro?".

## 6.bis Responsive design

Il portale deve essere **pienamente utilizzabile da desktop, tablet e smartphone**. Linee guida:

- **Layout fluido** con CSS Grid/Flex, breakpoint principali a 640px (mobile), 1024px (tablet), 1280px+ (desktop).
- **Header/navigazione:** su mobile menu hamburger che mostra dropdown comune + link `/comuni` + logout in pannello collassabile. Su desktop barra orizzontale piena.
- **Tabella dati `/comune/[slug]/data`:**
  - Su desktop: tabella tradizionale con scroll orizzontale se serve, header sticky.
  - Su tablet: stesso layout ma con pageSize default ridotto (50) e meno colonne mostrate di default.
  - Su mobile: layout "card-stack" — ogni riga renderizzata come card verticale con label+valore per le 4-5 colonne più importanti (`Desc. Trec.`, `Quantità`, `Assistente`, `Data Prod.`); tap sulla card apre il drawer dettaglio.
- **Drawer dettaglio:** su desktop pannello laterale 480px, su mobile fullscreen (slide up).
- **Form (login, comuni, import):** input full-width su mobile, padding adeguato per touch (target 44×44px).
- **Filtri/paginazione:** su mobile chip orizzontali scrollabili invece di select, paginazione minimale (Prev / Page X / Next).
- **Test:** verifica visiva manuale + smoke E2E con viewport mobile (Playwright `iPhone 13`) per il flusso di login + visualizzazione dati.

Niente CSS framework esterni (Tailwind, Bootstrap) — CSS scritto a mano resta più snello dato il numero contenuto di schermate.

## 7. Performance

- 5k righe × 4 comuni = ~20k righe totali. SQLite gestisce alla grande.
- Query con index su `comune_id` + filtri = <50ms attesi.
- Paginazione server-side (`LIMIT/OFFSET`) per la tabella, non si caricano mai tutte le righe in memoria client.
- Se in futuro un singolo comune supera 50k righe: aggiungere virtual scrolling (es. `svelte-virtual`) o cursor-based pagination.

## 8. Deploy

- Repository **Git su GitHub privato**.
- Vercel collegato → auto-deploy su push `main`.
- Variabili ambiente Vercel:
  - `TURSO_DB_URL`
  - `TURSO_AUTH_TOKEN`
  - `COOKIE_SECRET` (random 32 bytes, per signing/verifica cookie sessione)
  - `INITIAL_USER_EMAIL` (default `salvatore.damato@circet.it`)
  - `INITIAL_USER_PASSWORD` (default `admin`)
- Migrazioni: file SQL `migrations/001_init.sql` eseguito manualmente la prima volta tramite script `scripts/migrate.ts` (`npm run migrate`). Stesso script usato per seed dell'utente iniziale (idempotente).

## 9. Esplicitamente fuori MVP (YAGNI)

- Multi-utente (UI per creare altri admin)
- Reset password via email
- Storico multipli per stesso comune (oggi: 1 import attivo → wipe completo)
- Sync automatico con XME
- Export dati (CSV / Excel / PDF) — verrà definito quando progetteremo lo "schema generato"
- Auto-detect comune dal nome file o dal contenuto
- Importazione foglio `Foglio1` (note di lavoro)

## 10. Struttura repository

```
toto/
├── package.json
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
├── .env.example
├── README.md
├── migrations/
│   └── 001_init.sql
├── scripts/
│   └── migrate.ts
├── src/
│   ├── app.html
│   ├── app.d.ts
│   ├── hooks.server.ts          # auth middleware
│   ├── lib/
│   │   ├── server/
│   │   │   ├── db.ts             # client Turso
│   │   │   ├── auth.ts           # session helpers, password hashing
│   │   │   ├── excel.ts          # parsing Excel (SheetJS)
│   │   │   ├── columns.ts        # mapping header Excel → snake_case
│   │   │   └── repositories/
│   │   │       ├── users.ts
│   │   │       ├── comuni.ts
│   │   │       ├── imports.ts
│   │   │       └── rows.ts
│   │   └── components/
│   │       ├── DataTable.svelte
│   │       ├── ColumnPicker.svelte
│   │       ├── RowDrawer.svelte
│   │       ├── ComuneSwitcher.svelte
│   │       └── Toast.svelte
│   └── routes/
│       ├── +layout.svelte
│       ├── +layout.server.ts     # carica utente + lista comuni
│       ├── +page.svelte          # dashboard globale
│       ├── login/
│       ├── cambia-password/
│       ├── logout/
│       ├── comuni/
│       └── comune/
│           └── [slug]/
│               ├── +layout.server.ts   # carica comune attivo o 404
│               ├── +page.svelte
│               ├── import/
│               └── data/
└── docs/
    ├── Estrazioen totale.xlsx
    ├── Enrico_Marchesini_2026_Marzo_2026_Aprile.xlsx
    └── plans/
        └── 2026-05-02-toto-design.md
```

## 11. Prossimo passo

Invocare la skill `superpowers:writing-plans` per produrre il piano implementativo dettagliato (ordine di esecuzione, milestone, criteri di accettazione).
