# Migrazioni DB — CHANGELOG

Ordine di applicazione delle migrazioni. **In locale** si applicano via CLI;
**in produzione** si incolla l'SQL in phpMyAdmin (nessun runner runtime).

> Applicare i file in ordine numerico crescente. Ogni file è idempotente solo
> nel senso che va eseguito una volta su un DB pulito/aggiornato fino al numero
> precedente. Annotare qui ogni nuova migrazione.

### Ordine di applicazione completo (incollare in phpMyAdmin in QUESTA sequenza)

1. `database/migrations/001_init.sql` — schema (19 tabelle)
2. `database/migrations/002_no_varianti_colturali.sql` — rimuove le varianti colturali (varietà → note)
3. `database/migrations/003_interventi.sql` — modulo interventi (`tipi_intervento`, `interventi`)
4. `database/migrations/004_tipi_attivita.sql` — lookup `tipi_attivita` + `attivita.tipo_attivita_id`
5. `database/migrations/005_interventi_quantita.sql` — `interventi.quantita` + `interventi.unita` (opzionali)
6. `database/migrations/006_interventi_eseguito_fk.sql` — `interventi.eseguito_da` da VARCHAR a FK `users.id`
7. `database/migrations/007_interventi_drop_costo.sql` — rimuove la colonna `interventi.costo`
8. `database/migrations/008_allegati.sql` — tabella polimorfica `allegati` (foto di interventi/elementi)
9. `database/migrations/009_remember_tokens.sql` — tabella `remember_tokens` ("ricordami" persistente, login 7 giorni)
10. `database/seeds/001_seed.sql` — dati iniziali (settimane, sezioni, categorie spese, admin)
11. `database/seeds/002_seed_interventi.sql` — tipi di intervento iniziali
12. `database/seeds/003_seed_tipi_attivita.sql` — tipi di attività iniziali

I **seed** vanno applicati **dopo** le migrazioni. In produzione: aprire phpMyAdmin,
selezionare il database e incollare il contenuto di ciascun file nell'ordine qui sopra
(prima tutte le migrazioni, poi i seed). In locale: `mysql ... orto < <file>`.

---

## 009_remember_tokens.sql
Login persistente **"ricordami"** (7 giorni), indipendente dalla garbage collection delle
sessioni PHP (inaffidabile su hosting condiviso OVH).
- **`remember_tokens`**: `user_id` (FK → `users`, **ON DELETE CASCADE**), `token_hash` CHAR(64)
  (sha256, mai il token in chiaro), `expires_at` DATETIME, `created_at`; `UNIQUE(token_hash)`
  + indici su `user_id` ed `expires_at`.
- **Flusso** (lato app, `RememberMeService`): al login (verifica magic link) si genera un token
  casuale (`random_bytes(32)`), si salva l'hash con `expires_at = ora + 7 giorni` e si imposta un
  cookie `remember` (HttpOnly, Secure in produzione, SameSite=Lax, path=/, durata 7 giorni). Nel
  bootstrap, se non c'è sessione attiva ma il cookie è valido, l'utente viene ri-autenticato, la
  sessione rigenerata e il **token ruotato** (nuovo token+scadenza+cookie a ogni uso). Al logout
  si elimina cookie + riga; i token scaduti vengono ripuliti (`purgeExpired`).
- **NON DISTRUTTIVA**: nuova tabella isolata; nessuna modifica alle tabelle esistenti.

---

## 008_allegati.sql
Tabella **polimorfica** `allegati` per il caricamento di **foto** su interventi ed elementi
(coltivazioni / tipi animale).
- **`allegati`**: `entita_tipo` ENUM(`intervento`,`elemento`), `entita_id`, `percorso` (relativo a
  `storage/uploads/`, es. `2026/06/<hash>.jpg`), `nome_originale`, `mime`, `dimensione` (byte),
  `created_at`; indice `idx_allegati_entita (entita_tipo, entita_id)`.
- **Niente FK** (legame polimorfico): la cancellazione delle righe **e dei file fisici** è gestita
  lato applicazione (`AllegatoService::deleteForEntita`) quando si elimina l'intervento/elemento.
- **Storage**: solo immagini (`image/jpeg`, `image/png`, `image/webp`), MIME validato lato server
  con `finfo`, max ~8 MB per file. I file stanno in `storage/uploads/AAAA/MM/` (fuori da `public/`)
  con nome casuale; sono serviti **solo** dalla rotta autenticata `/allegati/{id}` (stream con
  `Content-Type` corretto, mai accessibili da URL pubblico).
- **NON DISTRUTTIVA**: nuova tabella isolata; nessuna modifica alle tabelle esistenti.

---

## 007_interventi_drop_costo.sql
Rimuove **definitivamente** la colonna `interventi.costo`: il costo degli interventi non si
gestisce più (i costi si registrano nel modulo **Spese**). La colonna era già NULL sui nuovi
interventi (vedi 006). **Distruttiva** ma senza FK/indici → `ALTER TABLE interventi DROP COLUMN
costo`. Rimossi anche tutti i riferimenti lato app (model INSERT/UPDATE, `costoTotaleElemento`,
controller, lista/timeline/dashboard e l'export PDF, dove la sezione diventa "Interventi
sull'elemento" con la quantità al posto del costo).

---

## 006_interventi_eseguito_fk.sql
**`interventi.eseguito_da`** passa da testo libero (`VARCHAR(150)`) a **FK verso `users.id`**:
l'esecutore si sceglie tra admin + collaboratori (default nel form: utente loggato).
- **DATI**: le poche voci testuali esistenti **non** sono mappabili a un utente e si azzerano
  (`UPDATE interventi SET eseguito_da = NULL`) prima del cambio di tipo.
- **Colonna**: `MODIFY eseguito_da INT UNSIGNED NULL` (combacia con `users.id`).
- **FK** `fk_interventi_eseguito`: **ON DELETE SET NULL** (rimuovendo un utente l'intervento resta
  senza esecutore) / ON UPDATE CASCADE; più indice `idx_interventi_eseguito`.
- **NB**: la colonna `interventi.costo` resta nel DB ma **non è più gestita dal form** (i costi si
  registrano nel modulo Spese); sui nuovi interventi è sempre NULL.

---

## 005_interventi_quantita.sql
Quantità opzionale sugli **interventi** (CLAUDE.md §5.8): quanto è stato fatto in un intervento.
- **`interventi.quantita`**: nuova colonna `DECIMAL(10,2) NULL`.
- **`interventi.unita`**: nuova colonna `VARCHAR(20) NULL` (unità di misura libera; valori
  suggeriti nel form: piante, kg, numero, litri). L'unità si scarta se la quantità è vuota.
- **NON DISTRUTTIVA**: entrambe nullable; gli interventi esistenti restano con `quantita`/`unita`
  NULL e le viste/report esistenti non si rompono.
- Nel form l'unità viene precompilata in base al tipo (Trapianto/Semina → "piante", Raccolta →
  "kg"), restando modificabile.

---

## 004_tipi_attivita.sql
Lookup configurabile **`tipi_attivita`** (analogo a `tipi_intervento`), per proporre nel
form "Nuova attività" le lavorazioni comuni filtrate per ambito della sezione.
- **`tipi_attivita`**: `nome`, `ambito` ENUM(colturale/animale/generico) **NULL** (NULL = proposto
  ovunque), `ordine`, `attivo`.
- **`attivita.tipo_attivita_id`**: nuova colonna FK NULL → `tipi_attivita(id)`, **ON DELETE SET NULL**
  / ON UPDATE CASCADE. `attivita.nome` resta sempre valorizzato (dal tipo scelto o dal nome
  personalizzato "Altro…"), così calendario e viste esistenti non si rompono.
- **NON DISTRUTTIVA / FK-SAFE**: le attività già create mantengono il loro `nome` con
  `tipo_attivita_id` NULL; cancellando un tipo le attività collegate perdono solo il riferimento.
- Seed dei tipi in `database/seeds/003_seed_tipi_attivita.sql` (colturale: Semenzaio, Semina diretta,
  Trapianto, Concimazione, Sarchiatura, Irrigazione, Trattamento, Diradamento, Raccolta, Potatura;
  animale: Incubazione, Schiusa).

---

## 003_interventi.sql
Modulo **interventi** (CLAUDE.md §5.8), distinto dalle `attivita` stagionali.
- **`tipi_intervento`**: lookup configurabile (`nome`, `ambito` colturale/animale/generico, `ordine`, `attivo`).
- **`interventi`**: `luogo_id` (FK RESTRICT), `tipo_intervento_id` (FK RESTRICT), `data`,
  `elemento_id` opzionale (FK SET NULL), `descrizione`, `costo` opzionale, `eseguito_da`, `created_at`.
- Seed dei tipi in `database/seeds/002_seed_interventi.sql`
  (Concimazione, Pulizia, Irrigazione, Trattamento, Manutenzione, Raccolta).

---

## 002_no_varianti_colturali.sql
Le **varianti** restano solo per il modello `animale` (razze). Per il colturale le
varietà si annotano nelle note. La migrazione: (1) riporta i nomi delle varianti
colturali esistenti nelle `note` dell'elemento (`GROUP_CONCAT`), (2) le elimina.
**FK-safe**: `attivita_storico.variante_id` è `ON DELETE SET NULL` (le voci di storico
colturali perdono il riferimento, come voluto); `gruppi.variante_id` riguarda solo
elementi animale e non viene toccato. **Idempotente** (no-op se non ci sono varianti colturali).

---

## 001_init.sql
Schema iniziale completo (sezione 5 di `CLAUDE.md`). **19 tabelle**, tutte
`InnoDB` / `utf8mb4` (collation `utf8mb4_unicode_ci`).

**Tabelle create (in ordine di dipendenza):**

| # | Tabella | Note |
|---|---|---|
| 1 | `sezioni` | macro-sezioni configurabili (modello colturale/animale) |
| 2 | `luoghi` | luoghi condivisi (ambito colturale/animale/generico) |
| 3 | `settimane` | tabella di riferimento 52 settimane (PK `numero`, da seed) |
| 4 | `users` | utenti + ruolo (admin/editor/viewer), email UNIQUE |
| 5 | `magic_tokens` | token magic-link (solo `token_hash` sha256, scadenza, monouso) |
| 6 | `preferenze_utente` | toggle calendario per utente (JSON) |
| 7 | `elementi` | coltivazione / tipo animale (FK `sezione_id`) |
| 8 | `varianti` | varietà / razza (FK `elemento_id`) |
| 9 | `attivita` | lavorazione / attività (FK `elemento_id`) |
| 10 | `attivita_settimane` | calendario verde/giallo, `UNIQUE(attivita_id, settimana)` + FK a `settimane` |
| 11 | `attivita_risorse` | link/guide/video (FK `attivita_id`) |
| 12 | `attivita_storico` | diario annuale (FK attività/variante/luogo) |
| 13 | `gruppi` | gruppi animali (FK elemento/variante/luogo) |
| 14 | `gruppo_movimenti` | log movimenti datati (consistenza = somma); self-FK `gruppo_collegato_id` |
| 15 | `mangimi` | anagrafica mangimi + soglia scorta |
| 16 | `mangimi_acquisti` | entrate magazzino (kg, costo) |
| 17 | `mangimi_somministrazioni` | uscite magazzino verso gruppi |
| 18 | `spese_categorie` | categorie spesa configurabili |
| 19 | `spese` | spese (FK categoria, FK opzionale sezione) |

**Convenzioni FK `ON DELETE`** (6 CASCADE · 10 RESTRICT · 6 SET NULL):
- **RESTRICT** dove cancellare romperebbe storico/dati con valore: `elementi→sezioni`,
  `gruppi→elementi/luoghi`, `attivita_storico→attivita`, `gruppo_movimenti→gruppi`,
  `mangimi_acquisti/somministrazioni→mangimi`, `mangimi_somministrazioni→gruppi`,
  `spese→spese_categorie`, `attivita_settimane→settimane`.
  La regola di dominio "i gruppi si chiudono, non si cancellano" è così rafforzata.
- **CASCADE** dove il figlio è sotto-configurazione/parte del padre: `varianti→elementi`,
  `attivita→elementi`, `attivita_settimane/attivita_risorse→attivita`,
  `magic_tokens/preferenze_utente→users`.
- **SET NULL** sui riferimenti opzionali (il dato resta, si annulla il legame):
  `attivita_storico→varianti/luoghi`, `gruppi→varianti`,
  `gruppo_movimenti→gruppo_collegato/luoghi`, `spese→sezioni`.
- `ON UPDATE CASCADE` su tutte le FK.

**Vincoli/indici notevoli:**
- `UNIQUE(attivita_id, settimana)` su `attivita_settimane` + indice su `settimana`.
- `users.email` UNIQUE; `magic_tokens.token_hash` UNIQUE; indice su `expires_at`.
- Indici espliciti su tutte le colonne FK e sulle colonne `data`/`anno`/`tipo` usate nei report.
- `gruppo_movimenti.quantita` è `INT UNSIGNED` (≥0 garantito dal tipo).
- CHECK su `settimane` (`numero` 1–52, `mese` 1–12).
- **Nota MySQL 8**: nessun CHECK su `attivita_settimane.settimana` — vietato su colonne
  in FK con referential action (errore 3823); la FK verso `settimane(numero)` già garantisce 1–52.

**Verifica:** applicata e testata su MySQL 8.0.45 (istanza usa-e-getta) — 19/19 tabelle,
tutte le FK con le regole attese, vincoli UNIQUE/FK/UNSIGNED e CASCADE/RESTRICT verificati.

**Seed richiesti dopo questa migrazione** → forniti da `database/seeds/001_seed.sql`.

---

## 001_seed.sql (seed — applicare dopo 001_init.sql)
Dati iniziali. Da eseguire **una sola volta** su DB pulito.

- **`settimane`**: 52 righe. Generate dallo script locale
  `database/seeds/gen_settimane.php` (1° gen + 7 giorni a settimana; i giorni
  residui di fine anno — 31 dic e, negli anni bisestili, 29 feb — sono assorbiti
  estendendo la settimana 52 fino al 31 dic). Date nominali su anno non bisestile
  (2025); `mese` = mese prevalente; `etichetta` es. `Apr · sett. 2`.
  Per rigenerare il blocco: `php database/seeds/gen_settimane.php`.
- **`sezioni`**: Orto (colturale), Animali (animale).
- **`spese_categorie`**: Carburante, Materiali, Attrezzature, Riparazioni, Consumabili.
- **`users`**: admin iniziale `matteo@disetti.it` (ruolo admin, attivo).

**Verifica:** applicato su MySQL 8.0.45 dopo la migrazione — conteggi 52/2/5/1;
settimane con numeri 1–52 distinti, copertura 1 gen → 31 dic, nessun salto di
continuità, `mese` coincidente col mese prevalente ricalcolato via SQL.
