Proposta di sviluppo — Modulo Germinabilità TopSeed
Analisi dettagliata dello schema DB esteso, motivazioni per ogni scelta, roadmap operativa
- 1. Principi di progettazione
- 2. Stato attuale del modulo TopSeed esistente
- 3. Schema esteso — campo per campo con motivazioni
- 3.1 Estensione tabella
categories - 3.2 Estensione tabella
lots - 3.3 Estensione tabella
produttori - 3.4 Estensione tabella
users - 3.5 Estensione tabella
test_germinabilita(30+ campi) - 3.6 Nuova tabella
test_germinabilita_conte - 3.7 Nuova tabella
tipologie_prova - 3.8 Nuova tabella
tipologie_seme - 3.9 Nuova tabella
specie_calendario_analisi
- 3.1 Estensione tabella
- 4. API endpoints custom
- 5. Roadmap 8 sprint — dettagli operativi
- 6. Scelte tecniche con motivazioni
- 7. Rischi con mitigazioni puntuali
- 8. Decisioni da prendere prima di partire
1. Principi di progettazione
1.1 Convivenza con modulo TopSeed esistente, non Big Bang
Il file germinabilita.html già esiste in TopSeed e mostra una struttura con due macro-tab: "🌱 Nuovo modulo" (scheletro vuoto creato da me come preparazione a questa integrazione) e "📜 Germinabilità OLD" (la consultazione legacy che abbiamo già costruito). Il lavoro consiste nel riempire di contenuto reale il tab "Nuovo modulo", non creare un'altra pagina da zero.
Motivazione: evitiamo che gli utenti TopSeed attuali debbano imparare una nuova URL o una nuova navbar. Il modulo appare dove si aspettano. La transizione è invisibile: l'utente che apriva germinabilita.html per vedere i placeholder, oggi trova le analisi reali.
1.2 Data model first, UI second
Tentare di costruire UI sopra uno schema incompleto porta a doppi giri di lavoro. Prima definiamo TUTTI i campi DB (anche quelli che compaiono solo in 1-2 schermate), poi sopra costruiamo. Questo è il Sprint 0 (fondamenta).
Motivazione: ogni ALTER TABLE post-UI rischia rotture nelle validazioni JS, nei filtri query, nelle serializzazioni. Meglio 3 giorni iniziali di "solo DB" che riscritture UI ricorrenti.
1.3 Feature parity prima, estensioni dopo
Sprint 0-7 mirano a replicare esattamente Semiorto (stesse pagine, stessi campi, stesse regole). Niente innovazioni. Solo dopo il go-live aggiungiamo cose che Semiorto non ha: email/WhatsApp, audit log, multi-azienda, esportazione API REST pubblica.
Motivazione: il laboratorio usa Semiorto da anni. Stravolgere la UX durante la migrazione = utenti che non trovano le cose = fallimento percepito. Replicare 1:1 è noioso ma riduce l'attrito al minimo.
1.4 Zero interruzioni lab durante la migrazione
Fino al giorno del taglio (fine Sprint 7), il laboratorio Semiorto continua a usare l'app di Diego su AWS. Il nostro modulo è "beta in costruzione" e non riceve dati reali.
Motivazione: se trasferiamo utenti progressivamente avremmo bisogno di sync bidirezionale (TopSeed → AWS e viceversa), che è molto complesso da mantenere. Meglio un taglio netto: Semiorto è fonte di verità fino al giorno X, TopSeed dal giorno X+1.
1.5 Sprint da 3-5 giorni con deliverable dimostrabile
Ogni sprint produce qualcosa che si può aprire nel browser e toccare. Non "backend 50%, frontend 50%". Si segue il principio "verticale": una feature completa end-to-end alla volta.
2. Stato attuale del modulo TopSeed esistente
Prima di scrivere lo schema esteso, bisogna capire cosa c'è GIÀ nel DB TopSeed. Ho verificato quanto segue interrogando il DB di produzione.
2.1 Tabella test_germinabilita attuale
Nel commit iniziale TopSeed questa tabella ha solo uno scheletro (creata come placeholder nella fase "Sprint E — Germinabilità standalone" di qualche mese fa). Campi attuali:
CREATE TABLE test_germinabilita (
id VARCHAR(50) NOT NULL PRIMARY KEY, -- ex UUID, stile TopSeed
lotId VARCHAR(50), -- FK → lots.id
tipo ENUM('contratto','commerciale') DEFAULT 'contratto',
specieRichiesta VARCHAR(255),
varietaRichiesta VARCHAR(255),
stato ENUM('in_coda','in_corso','completato','annullato') DEFAULT 'in_coda',
germinabilitaPercentuale INT,
esito ENUM('idoneo','non_idoneo','non_valutabile'),
sogliaMin INT DEFAULT 85,
dataRichiesta DATETIME,
dataCompletamento DATETIME,
richiestoDa VARCHAR(50), -- FK → users.id
assegnatoA VARCHAR(50), -- FK → users.id (ruolo GENETISTA)
note TEXT,
refertoImg LONGTEXT -- base64 del PDF/foto caricato
);
Questo schema è molto semplificato rispetto a Semiorto: non c'è l'FT, non c'è la tipologia seme, non c'è prova (PETRI/TORBA), non ci sono le conte giornaliere, non c'è scostamento, rif, semePuro, ecc.. È pensato come "richiesta test commerciale" più che come processo di laboratorio strutturato.
2.2 Record attuali in produzione
La tabella contiene pochissimi record di test (demo). Non c'è storico produttivo. Questo è un vantaggio: possiamo allargarla senza timore di rompere dati reali.
2.3 Tabella lots attuale
CREATE TABLE lots (
id VARCHAR(50) PRIMARY KEY,
num VARCHAR(50), -- codice lotto testuale
sId VARCHAR(50), -- FK → seeds.id
ctId VARCHAR(50), -- FK → contracts.id (contratto produzione)
qty DOUBLE, -- quantità in unità TopSeed
qtyUnit VARCHAR(10), -- 'kg' | 'g'
stato ENUM('in_stock','riservato','scalato','esaurito','scartato'),
note TEXT
);
Campi rilevanti: c'è già num (codice lotto), qty (quantità), stato. Mancano: dataArrivo, provenienza, dataEsaurito, dataProssimaAnalisi, id_produttore (lotti TopSeed hanno ctId non produttore: diversa logica).
In TopSeed i lotti sono legati a contratti di produzione (modello agricolo: faccio un contratto con un produttore per ricevere X kg di seme). In Semiorto i lotti sono legati direttamente a un produttore (modello laboratoriale: ricevo seme da un produttore, lo analizzo). Sono due modelli diversi.
Soluzione: il lotto TopSeed avrà sia ctId (opzionale, per lotti da contratto) sia produttore_id (opzionale, per lotti legacy Semiorto o lotti senza contratto). Uno dei due deve essere valorizzato.
2.4 Tabella categories attuale
CREATE TABLE categories (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL -- es. "Pomodoro"
);
Scarsa: solo nome. Semiorto ha 160 specie con tanti metadati (italiano/inglese/latino/codice/giorni1Conta/giorniTConta/UR). Tutti da aggiungere.
2.5 Tabella produttori attuale
CREATE TABLE produttori (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255),
-- + campi anagrafica base (indirizzo, partitaIva, email, ecc.)
aliquotaIvaAutofattura DECIMAL(5,2)
);
Manca codice (Semiorto ha un codice breve tipo "SF" per Schiavone Fernando, usato in generaLotto e nelle stampe).
2.6 Tabella users attuale
Ha già: id, username, password (bcrypt), nome, cognome, email, role, attivo. Manca: legacy_sha_hash per preservare gli hash Semiorto durante la migrazione (così al primo login Semiorto post-migrazione l'utente può loggare con la vecchia password).
3. Schema esteso — campo per campo con motivazioni
3.1 Estensione tabella categories
ALTER TABLE categories
ADD COLUMN italiano VARCHAR(60),
ADD COLUMN inglese VARCHAR(60),
ADD COLUMN latino VARCHAR(60),
ADD COLUMN codice_specie VARCHAR(10),
ADD COLUMN giorni_1_conta INT,
ADD COLUMN giorni_tot_conta INT,
ADD COLUMN ur_target INT,
ADD COLUMN note_specie VARCHAR(512),
ADD COLUMN legacy_source VARCHAR(32),
ADD COLUMN legacy_id INT;
Motivazioni campo-per-campo:
italianoVARCHAR(60)- Perché: in Semiorto le specie hanno 3 nomi paralleli (italiano, inglese, latino) perché i certificati DOCX per clienti esteri usano il nome inglese e latino. Dove viene usato:
AnalisiController.certificato()chiamabo.getLotto().getSpecie().getNome("en")egetNome("lat"). Perché non fonderlo conname: TopSeed già usanamegenericamente. Tenere separatoitalianopermette di distinguere "il nome primario visualizzato" (name, spesso = italiano) dai tre linguistici. In fase di migrazione, senamecoincide conitalianosappiamo che la specie è pulita. inglese,latinoVARCHAR(60)- Perché: solo per stampe multilingue. Nessun altro uso.
codice_specieVARCHAR(10)- Perché: codice breve (es. "POM" per Pomodoro) usato in Semiorto dentro
generaLotto()(che è dead code, ma il concetto di codice breve resta comodo per UI compatta). Lo manteniamo per permettere (in futuro) un'eventuale feature di auto-generazione codice lotto, o per avere un riferimento più leggibile dell'ID numerico. giorni_1_contaINT- Perché CRUCIALE: definisce quando il tecnico deve fare la PRIMA lettura dopo l'inizio dell'analisi. Dove usato:
AnalisiController.nuovaAnalisi()pre-compila la conta A con questo valore.AnalisiServiceImpl.inCorso()calcola la "prossima operazione" aggiungendo questo numero adataInizio. Esempio: Pomodoro = 5 giorni (prima conta), Peperone = 7 giorni. giorni_tot_contaINT- Perché CRUCIALE: definisce quando deve finire la prova (giorno della lettura finale). Dove usato: pre-compilazione conta A, calcolo "seconda operazione", e soprattutto dentro la query
daAnalizzare()come filtro:DATE_ADD(dataChiusura, INTERVAL (dayOfMonth - giorniTConta) DAY) < firstOfMonth. Senza questo campo la query "Da Analizzare" non funziona. ur_targetINT- Perché: umidità relativa % consigliata per la germinazione di quella specie. Pre-compilato nel form analisi come default per il campo
ur. Non è obbligatorio, serve solo a ridurre errori di inserimento. note_specieVARCHAR(512)- Perché: osservazioni particolari della specie (es. "germinazione lenta, spesso serve scarificazione"). Mostrato come help-text nel form nuova analisi. Dove usato: in
AnalisiController.nuovaAnalisi()viene passato al model con chiavenote. legacy_sourceVARCHAR(32)- Perché: marcatore di provenienza. Valori:
'diego'per record importati da Semiorto,NULLper specie create in TopSeed nativamente,'shared'se una specie esiste in entrambi ed è stata mergeata. Ci permette dopo la migrazione di capire l'origine di ogni record. legacy_idINT- Perché: traccia l'ID originale in
semiorto_specie. Serve per audit trail e per riconciliare problemi nella migrazione (se qualcosa non torna, possiamo re-interrogare il DB legacy con quell'ID).
3.2 Estensione tabella lots
ALTER TABLE lots
ADD COLUMN produttore_id VARCHAR(50) NULL,
ADD COLUMN kg DOUBLE,
ADD COLUMN provenienza VARCHAR(60),
ADD COLUMN data_arrivo DATE,
ADD COLUMN data_prossima_analisi DATE,
ADD COLUMN data_esaurito DATE,
ADD COLUMN esaurito TINYINT(1) DEFAULT 0,
ADD COLUMN legacy_id INT,
ADD COLUMN legacy_source VARCHAR(32),
ADD INDEX idx_esaurito (esaurito),
ADD INDEX idx_data_arrivo (data_arrivo),
ADD INDEX idx_legacy_id (legacy_id),
ADD CONSTRAINT fk_lots_produttore FOREIGN KEY (produttore_id) REFERENCES produttori(id) ON DELETE SET NULL;
produttore_idVARCHAR(50)- Perché NULLABLE: i lotti TopSeed esistenti sono legati a
ctId(contratto), non a produttore diretto. I lotti Semiorto importati hannoproduttore_idvalorizzato ectIdNULL. La regola business: uno dei due deve essere valorizzato. ON DELETE SET NULL: se un produttore viene cancellato, i lotti non spariscono, si orfanano. kgDOUBLE- Perché non usare
qtyesistente:qtyin TopSeed è in unità generiche (qtyUnit='g'o'kg'). Semiorto usa sempre kg come unità primaria. Manteniamokgseparato per evitare conversioni al volo durante import. In visualizzazione UI decidiamo: se c'èkgvalorizzato lo mostriamo, altrimenti formattiamoqty + qtyUnit. provenienzaVARCHAR(60)- Perché: testo libero che identifica il sito di origine del seme (es. "Piana del Sele", "Campo 3 Calabria"). Separato dal nome del produttore perché un produttore può avere più siti. Dove usato: stampe certificato, ricerche.
data_arrivoDATE- Perché: giorno in cui il lotto è stato ricevuto dal magazzino. Usato per: ordinamento liste lotti (default: più recenti prima), filtri ricerca, calcolo "validità" del lotto (se dopo N giorni senza analisi diventa "da analizzare").
data_prossima_analisiDATE- Perché: data suggerita per la prossima analisi periodica. Viene popolata automaticamente alla chiusura di un'analisi:
data_prossima_analisi = data_chiusura + giorni_tot_conta + 30(indicativo). L'operatore la può modificare. data_esauritoDATE- Perché: quando si imposta
esaurito=1, il sistema valorizza automaticamentedata_esaurito=CURDATE(). Riportando il lotto ad attivo (raro)data_esaurito=NULL. esauritoTINYINT(1)- Perché non usare lo stato ENUM esistente:
lots.statoha già un valore'esaurito'. Ma il campoesauritoè un FILTRO QUERY usato moltissimo ("solo lotti non esauriti per nuove analisi", "solo esauriti per archivio"). Avere untinyintindicizzato è più veloce di un filtro sul VARCHARstato. Quando stato='esaurito' ↔ esaurito=1. Trade-off: denormalizzazione controllata. legacy_id,legacy_source- Come per categories: audit trail migrazione.
3.3 Estensione tabella produttori
ALTER TABLE produttori
ADD COLUMN codice VARCHAR(45),
ADD COLUMN legacy_id INT,
ADD COLUMN legacy_source VARCHAR(32);
codiceVARCHAR(45)- Perché: codice identificativo breve (es. "SF", "SR", "VSF"). Usato in Semiorto per stampe compatte e (teoricamente) nel
generaLotto. In TopSeed lo usiamo per: pulsanti nel dropdown produttori più leggibili ("[SF] Schiavone Fernando"), future feature di auto-code lotto.
3.4 Estensione tabella users
ALTER TABLE users
ADD COLUMN legacy_sha_hash VARCHAR(255),
ADD COLUMN legacy_source VARCHAR(32),
ADD COLUMN legacy_id INT,
ADD COLUMN cambia_password TINYINT(1) DEFAULT 0;
legacy_sha_hashVARCHAR(255)- Perché: Semiorto usa SHA-1 (
ShaPasswordEncoder). TopSeed usa bcrypt. Durante la migrazione importiamo l'hash SHA-1 inlegacy_sha_hashe lasciamopassword(bcrypt) vuoto. Al primo login post-migrazione:- L'utente digita password
- Se
legacy_sha_hashvalorizzato e match SHA-1 → ok, RIGENERA bcrypt, salva inpassword, svuotalegacy_sha_hash - Se no match → errore credenziali
cambia_passwordTINYINT(1)- Perché: replica il flusso Semiorto dove al primo login (post-creazione admin o post-reset) si forza cambio password. Flag booleano: se 1, al login prossimo l'utente viene redirezionato a
/cambio-passwordforzato.
3.5 Estensione tabella test_germinabilita (il pezzo grosso)
Qui c'è la parte più massiccia. Partiamo dallo schema TopSeed attuale (vedi §2.1) e aggiungiamo 30+ campi per catturare tutto quello che Semiorto registra.
ALTER TABLE test_germinabilita
-- Identificativo laboratorio
ADD COLUMN ft VARCHAR(45),
-- FK tecnico
ADD COLUMN tecnico_id VARCHAR(50),
-- Tipologia prova e seme (FK verso nuove lookup)
ADD COLUMN tipologia_prova_id INT,
ADD COLUMN tipologia_seme_id INT,
ADD COLUMN replica_analisi INT,
ADD COLUMN calibratura VARCHAR(45),
-- Date workflow
ADD COLUMN data_arrivo_laboratorio DATE,
ADD COLUMN data_inizio DATE,
ADD COLUMN data_chiusura DATE,
-- Parametri ambientali/prova
ADD COLUMN scostamento ENUM('REGOLARE','IRREGOLARE'),
ADD COLUMN ur INT,
ADD COLUMN totale_giorni_prova INT,
-- Riferimento analisi esterna
ADD COLUMN rif_numero VARCHAR(45),
ADD COLUMN rif_data DATE,
ADD COLUMN rif_germinabilita INT,
ADD COLUMN rif_prova_laboratorio VARCHAR(45),
ADD COLUMN germinabilita_prova_laboratorio INT,
-- Esito
ADD COLUMN risultato ENUM('POSITIVO','NEGATIVO'),
ADD COLUMN vigore ENUM('SCARSO','MEDIO','OTTIMO'),
ADD COLUMN pillole INT,
-- Purezza
ADD COLUMN seme_puro DOUBLE DEFAULT 100,
ADD COLUMN materiale_inerte DOUBLE DEFAULT 0,
ADD COLUMN altri_semi DOUBLE,
ADD COLUMN altri_semi_string VARCHAR(512),
ADD COLUMN osservazioni_purezza VARCHAR(512),
-- Misc
ADD COLUMN categoria_analisi ENUM('STANDARD','COMMERCIALE') DEFAULT 'STANDARD',
ADD COLUMN osservazioni VARCHAR(512),
ADD COLUMN germinabilita INT,
ADD COLUMN cancella_note VARCHAR(128),
-- Audit
ADD COLUMN legacy_id INT,
ADD COLUMN source VARCHAR(32) DEFAULT 'topseed',
ADD INDEX idx_legacy_id (legacy_id),
ADD INDEX idx_source (source),
ADD INDEX idx_data_chiusura (data_chiusura),
ADD INDEX idx_ft (ft),
ADD INDEX idx_tecnico (tecnico_id),
ADD CONSTRAINT fk_tg_tecnico FOREIGN KEY (tecnico_id) REFERENCES users(id) ON DELETE SET NULL,
ADD CONSTRAINT fk_tg_tp FOREIGN KEY (tipologia_prova_id) REFERENCES tipologie_prova(id),
ADD CONSTRAINT fk_tg_ts FOREIGN KEY (tipologia_seme_id) REFERENCES tipologie_seme(id);
Motivazioni (raggruppate per aree logiche):
3.5.1 Identificativo laboratorio — ft
ftVARCHAR(45) — "Fattura tecnica"- Perché: numero progressivo annuale che identifica univocamente l'analisi agli occhi del cliente e del laboratorio. Formato
NNN/YY(es. "3562/26"). Come viene generato: max FT dell'anno + 1. Vedi calcoli §2. Impatto: stampato sul certificato, cercabile in tabella cerca, mostrato ovunque. Perché tipo VARCHAR: il separatore "/" rende inadatto un tipo numerico. 45 char = margine di sicurezza (in realtà max 7 char).
3.5.2 Tecnico responsabile — tecnico_id
tecnico_idVARCHAR(50)- Perché: ogni analisi è eseguita da un tecnico (GENETISTA). Il nome del tecnico appare sul certificato. Motivazione semantica: nello schema TopSeed già esistente c'è
richiestoDaeassegnatoA.tecnico_idè quello che chiude l'analisi e firma. Potrebbe coincidere conassegnatoA, ma li teniamo separati perché: chi prende in carico il test (assegnatoA) non sempre è chi lo chiude (tecnico_id). ON DELETE SET NULL: se un tecnico esce dall'azienda e viene cancellato, lo storico analisi non sparisce.
3.5.3 Tipologia prova, seme, replica — campi prova
tipologia_prova_idINT →tipologie_prova- Perché FK invece di ENUM: ENUM richiederebbe ALTER TABLE per ogni nuovo tipo. FK permette all'admin di aggiungere "TEST RAPIDO" o "SUBSTRATO ALTERNATIVO" senza migrazione DB. Valori iniziali: PETRI, TORBA, CONTENITORI ALVEOLATI.
tipologia_seme_idINT →tipologie_seme- Perché: stesso principio. Lookup table con 9 valori iniziali (Portaseme, Natura, Selezionato, Calibrato, Film coated, Pillolato, Trattato, Multiseme, Vigorizzato).
replica_analisiINT- Perché INT e non FK: in Semiorto i valori ammessi sono solo 1, 2, 3 (numero di ripetizioni programmate). Un ENUM o una lookup sarebbero over-engineering. Un INT con check constraint
> 0 AND < 4basta. calibraturaVARCHAR(45)- Perché: testo libero sulla calibratura del seme (es. "3.25-3.5 mm"). Dato tecnico che va sul certificato.
3.5.4 Date del workflow
data_arrivo_laboratorioDATE- Perché: giorno in cui il campione è arrivato in laboratorio dal magazzino. Può essere diverso da
lots.data_arrivo(il lotto è arrivato in magazzino il giorno X, il campione per l'analisi è stato prelevato e portato al lab il giorno Y > X). Impatto: stampato sul certificato come "Data di arrivo". data_inizioDATE- Perché: giorno di inizio effettivo della prova (semina nelle Petri). Spesso uguale a
data_arrivo_laboratorio, ma non sempre. Usato per calcolare: la "prossima operazione" nella lista In Corso (data_inizio + giorni_1_conta= data prima conta). data_chiusuraDATE (NULL = in corso)- Perché un campo che può essere NULL: è il discriminante tra analisi "In Corso" (
data_chiusura IS NULL) e "Chiusa" (IS NOT NULL). Impostato a CURDATE() al momento della chiusura. Perché DATE e non DATETIME: Semiorto usa solo la data (non l'ora). Coerenza.
3.5.5 Scostamento, UR, giorni prova
scostamentoENUM('REGOLARE','IRREGOLARE')- Perché: durante le letture il tecnico valuta se la prova sta andando REGOLARMENTE o si rilevano IRREGOLARITÀ (es. contaminazione, UR non mantenuta, temperatura anomala). Valore impostato alla chiusura. Se IRREGOLARE, il certificato riporta "prova con scostamento, interpretare con cautela".
urINT- Perché: UR (Umidità Relativa) effettiva mantenuta durante la prova, misurata al termine. Pre-compilato con
categories.ur_target, modificabile dal tecnico. Stampato sul certificato: sì. totale_giorni_provaINT- Perché: numero totale di giorni effettivi della prova. Pre-compilato con
categories.giorni_tot_conta. Viene registrato fisso all'avvio perché se la specie viene modificata dopo (nuova giorniTConta) non cambia lo storico.
3.5.6 Riferimento analisi esterna
Questi 5 campi servono se il cliente fornisce un'analisi eseguita da un altro laboratorio, per confronto. Il tecnico Semiorto riceve il seme già con un "certificato del fornitore" e può annotarlo.
rif_numeroVARCHAR(45)- Numero del certificato esterno.
rif_dataDATE- Data del certificato esterno.
rif_germinabilitaINT- % germinabilità dichiarata dal fornitore.
rif_prova_laboratorioVARCHAR(45)- Nome del laboratorio esterno.
germinabilita_prova_laboratorioINT- Quasi sempre uguale a
rif_germinabilitama in alcuni casi il tecnico annota il valore finale che risulta dalla loro stessa prova di laboratorio (diversa da quella del fornitore). Bug nel codice Semiorto:update()imposta entrambi i campi allo stesso valore (vo.setGerminabilitaProvaLaboratorio(bo.getAnalisiEsterna().getGerminabilita())). Replichiamo il bug ma lo segnaliamo all'admin — potrebbe essere un'ottimizzazione richiesta.
3.5.7 Esito
risultatoENUM('POSITIVO','NEGATIVO')- Perché: giudizio finale del tecnico. NON automatico da % germinabilità (un 84% potrebbe essere POS per una specie con soglia 80 e NEG per una con soglia 85). Dove usato: stampa certificato, filtri ricerca.
vigoreENUM('SCARSO','MEDIO','OTTIMO')- Perché: valutazione di vigore globale. Pre-compilato come media dei vigori delle singole conte (vedi calcoli §11), modificabile manualmente dal tecnico.
pilloleINT- Perché: per semi pillolati, numero totale di pillole. Dato tecnico, non sempre valorizzato.
3.5.8 Purezza
seme_puroDOUBLE DEFAULT 100- Perché DEFAULT 100: se non specificato, assumiamo seme al 100% puro. Rappresenta la % di semi della varietà dichiarata sul totale del campione.
materiale_inerteDOUBLE DEFAULT 0- Frammenti non-seme (terra, polvere, foglie secche).
altri_semiDOUBLE- % di semi di altre specie contaminanti.
altri_semi_stringVARCHAR(512)- Testo libero dettagliando quali semi contaminanti (es. "Amaranthus retroflexus, Chenopodium album").
osservazioni_purezzaVARCHAR(512)- Note aggiuntive sul test di purezza.
3.5.9 Metadati finali
categoria_analisiENUM('STANDARD','COMMERCIALE')- Perché: STANDARD = test di routine interno. COMMERCIALE = test con validità per certificazione ufficiale (soglie più alte, procedure più stringenti). Impatto su stampa e su eventuali controlli automatizzati futuri.
osservazioniVARCHAR(512)- Note libere generali sull'analisi.
germinabilitaINT- Perché STORED e non CALCULATED: la germinabilità viene calcolata server-side alla chiusura dell'analisi (media delle conte, normalizzata). Salvarla evita di ricalcolare ogni volta che si visualizza. In Semiorto è sempre stored, lo replichiamo.
cancella_noteVARCHAR(128)- Perché stringa e non bool: in Semiorto il campo
cancellacontiene il motivo della cancellazione (non solo un flag).NULL= analisi attiva, stringa valorizzata = analisi cancellata. Permette soft-delete con audit trail.
3.5.10 Audit trail
legacy_idINT,sourceVARCHAR(32) DEFAULT 'topseed'- Come per altre tabelle. Fondamentale per distinguere record importati da record nuovi post-migrazione.
3.6 Nuova tabella test_germinabilita_conte
CREATE TABLE test_germinabilita_conte (
id INT AUTO_INCREMENT PRIMARY KEY,
analisi_id VARCHAR(50) NOT NULL,
lettera CHAR(1) NOT NULL DEFAULT 'A',
giorni_1 INT,
germinati_1 INT, duri_1 INT, freschi_1 INT, anormali_1 INT, morti_1 INT,
vigore_1 ENUM('SCARSO','MEDIO','OTTIMO'),
giorni_2 INT,
germinati_2 INT, duri_2 INT, freschi_2 INT, anormali_2 INT, morti_2 INT,
vigore_2 ENUM('SCARSO','MEDIO','OTTIMO'),
FOREIGN KEY (analisi_id) REFERENCES test_germinabilita(id) ON DELETE CASCADE,
INDEX idx_analisi (analisi_id),
INDEX idx_lettera (lettera)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Motivazioni tabella
- Relazione 1:N con test_germinabilita
- Ogni analisi ha tipicamente 4 conte (una per ripetizione A, B, C, D). A volte più (E, F, G) se si aggiungono repliche. La tabella
conteha una riga per ciascuna ripetizione. - Perché ON DELETE CASCADE
- Se si elimina definitivamente un'analisi, tutte le sue conte vanno cancellate insieme. Non ha senso mantenere conte orfane. La modalità soft-delete (cancella_note) non tocca le conte, le mantiene.
letteraCHAR(1)- Ripetizione identificata da lettera A, B, C, D, ... Seguendo il pattern Semiorto. L'alternativa sarebbe un numero progressivo ma le lettere sono più immediatamente distinguibili nei grafici stampe.
Motivazioni campi 2 letture parallele
Per ogni ripetizione ci sono sempre 2 letture temporali: la prima parziale e la finale. Quindi ogni campo è duplicato: germinati_1 (prima lettura) e germinati_2 (seconda lettura).
giorni_1,giorni_2- Numero di giorni dall'inizio della prova al momento della rispettiva lettura. Es.
giorni_1=5, giorni_2=10significa "prima lettura il giorno 5, seconda al giorno 10". germinati_1,germinati_2- N° semi germinati (su 100 seminati) rilevati alla rispettiva lettura. Per la germinabilità finale si sommano le due letture (vedi calcoli).
duri_1/2- Semi che non si imbibiscono (tegumento impermeabile). Frequenti in leguminose.
freschi_1/2- Semi vivi ma non ancora germinati. In dormienza.
anormali_1/2- Plantule germinate ma con difetti (radici malformate). NON contate come "germinate" ai fini commerciali.
morti_1/2- Semi morti, decomposti.
vigore_1/2- Valutazione soggettiva del tecnico sulla vitalità delle plantule. ENUM 3 valori.
Invariante biologico: la somma di (germinati + duri + freschi + anormali + morti) in giorni_1 e giorni_2 deve essere 100 (= 100 semi seminati per ogni Petri). Se non torna, il sistema normalizza riassegnando a germinati_2.
3.7 Nuova tabella tipologie_prova
CREATE TABLE tipologie_prova (
id INT AUTO_INCREMENT PRIMARY KEY,
codice VARCHAR(30) UNIQUE NOT NULL,
nome VARCHAR(60) NOT NULL,
descrizione TEXT,
ordinamento INT DEFAULT 0,
attivo TINYINT(1) DEFAULT 1
);
INSERT INTO tipologie_prova (codice, nome, descrizione, ordinamento) VALUES
('PETRI', 'PETRI', 'Germinazione in capsula Petri su carta assorbente. La più comune.', 1),
('TORBA', 'TORBA', 'Germinazione in pane di torba. Per specie che richiedono substrato.', 2),
('CONT_ALV', 'CONTENITORI ALVEOLATI', 'Germinazione in vassoi con alveoli separati. Per sementi grosse o a sviluppo radicale importante.', 3);
- Perché tabella dedicata
- In Semiorto è una lista iniettata via Spring XML. Lista statica. Noi la mettiamo in DB per permettere all'admin di aggiungere nuovi tipi (es. se in futuro si introduce "Substrato idroponico") senza deploy. Il campo
attivopermette di disabilitare un tipo obsoleto senza cancellarlo (preserva i record storici). descrizioneTEXT- Testo help-text mostrato nella UI sotto il dropdown "Tipo prova", spiega all'operatore cosa significa ciascuna opzione.
ordinamentoINT- Permette all'admin di controllare l'ordine nel dropdown. Il default mette PETRI in cima (il più usato).
3.8 Nuova tabella tipologie_seme
CREATE TABLE tipologie_seme (
id INT AUTO_INCREMENT PRIMARY KEY,
codice VARCHAR(30) UNIQUE NOT NULL,
nome VARCHAR(60) NOT NULL,
descrizione TEXT,
ordinamento INT DEFAULT 0,
attivo TINYINT(1) DEFAULT 1
);
INSERT INTO tipologie_seme (codice, nome, descrizione, ordinamento) VALUES
('PORTASEME', 'Portaseme', 'Seme destinato a riproduzione.', 0),
('NATURA', 'Natura', 'Seme grezzo, non lavorato.', 1),
('SELEZIONATO', 'Selezionato', 'Seme pulito dai materiali inerti.', 2),
('CALIBRATO', 'Calibrato', 'Seme calibrato per dimensione uniforme.', 3),
('FILM_COATED', 'Film coated', 'Seme con rivestimento sottile protettivo.', 4),
('PILLOLATO', 'Pillolato', 'Seme incapsulato in pellet (più grosso, più facile semina meccanica).', 5),
('TRATTATO', 'Trattato', 'Seme trattato con fungicidi o insetticidi.', 6),
('MULTISEME', 'Multiseme', 'Pellet contenente più semi (es. lattughe).', 7),
('VIGORIZZATO', 'Vigorizzato', 'Seme pre-imbibito per accelerare germinazione.', 9);
Stessa logica di tipologie_prova. I 9 valori iniziali vengono direttamente dalla tabella semiorto_tipologia_seme letta con SQL live sul DB AWS.
3.9 Nuova tabella specie_calendario_analisi
CREATE TABLE specie_calendario_analisi (
id INT AUTO_INCREMENT PRIMARY KEY,
categoria_id VARCHAR(50) NOT NULL,
mese INT NOT NULL,
tipologia_seme_id INT NOT NULL,
UNIQUE KEY uq (categoria_id, mese, tipologia_seme_id),
INDEX idx_categoria (categoria_id),
INDEX idx_mese (mese),
FOREIGN KEY (categoria_id) REFERENCES categories(id) ON DELETE CASCADE,
FOREIGN KEY (tipologia_seme_id) REFERENCES tipologie_seme(id) ON DELETE CASCADE,
CHECK (mese BETWEEN 1 AND 12)
);
Motivazioni
- Equivalente a cosa Semiorto
- Replica la tabella legacy
semiorto_map_specie_analisi_ripetizione_tipologia_seme. Nome legacy fuorviante (perché "analisi_ripetizione" in realtà sono i 12 mesi dell'anno). Noi usiamo nomi espliciti:mese. - Semantica: "calendario di ripetizione analisi"
- Per ogni specie, l'admin spunta in quali mesi dell'anno è previsto un test di routine, e per quale tipologia seme. Esempio: "Pomodoro, a Maggio, per seme Selezionato" significa "ogni maggio ripetiamo il test su tutti i lotti di pomodoro selezionato attivi".
- Perché INT per mese invece di FK
- In Semiorto i mesi sono una tabella (
semiorto_analisi_ripetizione, 12 righe). Per noi è over-engineering: un INT 1-12 con check constraint è più leggibile nelle query e non richiede JOIN. - UNIQUE KEY su 3 campi
- Previene duplicati della stessa configurazione (stessa specie, stesso mese, stessa tipologia).
- ON DELETE CASCADE su entrambe le FK
- Se si cancella una specie → spariscono le sue configurazioni calendario. Idem tipologia seme. Il calendario è configurazione, non dato storico.
Dove viene usato: alimenta la query "Da Analizzare" (vedi flussi). Senza questa tabella popolata, la lista "Da Analizzare" è sempre vuota, anche se ci sono lotti attivi.
4. API endpoints custom
I CRUD standard sono già coperti da api.php esistente (basta aggiungere le nuove entità in $ENTITY_MAP). Per le logiche custom creiamo un nuovo file api_germinabilita.php.
4.1 ?action=in_corso — lista analisi in corso
SELECT tg.*,
l.num lotto_num, l.provenienza,
s.name seme_name,
c.italiano specie_name, c.giorni_1_conta, c.giorni_tot_conta,
u.nome tecnico_nome, u.cognome tecnico_cognome
FROM test_germinabilita tg
LEFT JOIN lots l ON l.id = tg.lotId
LEFT JOIN seeds s ON s.id = l.sId
LEFT JOIN categories c ON c.id = s.categoryId
LEFT JOIN users u ON u.id = tg.tecnico_id
WHERE tg.data_chiusura IS NULL
AND tg.cancella_note IS NULL
AND (l.esaurito IS NULL OR l.esaurito = 0)
ORDER BY tg.data_inizio DESC;
Ritorna JSON array. Usato nel Dashboard "In Corso" + badge navbar (con count).
4.2 ?action=da_analizzare — lotti che scadono questo mese
SELECT tg.* FROM test_germinabilita tg
JOIN (
SELECT MAX(a.id) id, l.id lot_id, c.giorni_tot_conta
FROM test_germinabilita a
JOIN lots l ON a.lotId = l.id
JOIN seeds s ON l.sId = s.id
JOIN categories c ON s.categoryId = c.id
JOIN specie_calendario_analisi cal ON cal.categoria_id = c.id
WHERE l.esaurito = 0
AND cal.mese = MONTH(CURDATE())
GROUP BY l.id
) q ON tg.id = q.id
WHERE tg.data_chiusura IS NOT NULL
AND DATE_ADD(tg.data_chiusura,
INTERVAL (DAY(LAST_DAY(CURDATE() - INTERVAL 1 MONTH)) - q.giorni_tot_conta) DAY)
< DATE_FORMAT(CURDATE(), '%Y-%m-01')
ORDER BY tg.id;
Replica esatta della query daAnalizzare() Semiorto, adattata allo schema TopSeed. Vedi spiegazione decodificata.
4.3 ?action=nuova_analisi&lot_id=X — crea analisi su lotto
Transazione:
- Verifica che il lotto esista e non sia esaurito
- Genera FT prossimo (vedi §4.4)
- INSERT
test_germinabilitacon: ft, lot_id=X, data_arrivo_laboratorio=CURDATE(), tutti gli altri NULL - Pre-popola conta A via INSERT
test_germinabilita_contecon giorni da specie - Ritorna l'id nuovo dell'analisi
4.4 ?action=genera_ft — prossimo FT disponibile
-- Usa FOR UPDATE per evitare race conditions
BEGIN;
SELECT MAX(CAST(SUBSTRING_INDEX(ft, '/', 1) AS UNSIGNED)) + 1 AS prossimo
FROM test_germinabilita
WHERE ft LIKE CONCAT('%/', YEAR(CURDATE()) % 100)
FOR UPDATE;
-- Ritorna: "3562/26"
4.5 ?action=chiudi_analisi&id=X
- Verifica tutte le conte completate (giorni_2 valorizzato per tutte)
- Chiama
ricalcola_germinabilita(X)che esegue la formula della media (vedi calcoli) - Legge dal payload: risultato, scostamento, vigore, tecnico_id
- UPDATE analisi con data_chiusura=CURDATE() + valori
- Ritorna analisi aggiornata
4.6 ?action=ricalcola_germinabilita&id=X
Chiamabile anche prima della chiusura per anteprima. Riproduzione esatta dell'algoritmo generaConta() di Semiorto. Vedi calcoli.
4.7 ?action=cancella_analisi
POST { id: "X", motivo: "Errore nel lotto" }
-- UPDATE test_germinabilita SET cancella_note = :motivo WHERE id = :id
4.8 ?action=ripristina_analisi
POST { id: "X" }
-- UPDATE test_germinabilita SET cancella_note = NULL WHERE id = :id
Solo ADMIN (controllo lato PHP su session).
4.9 ?action=certificato&id=X&azienda=SEMIORTO
GET. Genera PDF con template engine. Vedi Sprint 5.
4.10 ?action=scheda&id=X
GET. Genera PDF scheda di controllo.
4.11 ?action=stats
SELECT
(SELECT COUNT(*) FROM test_germinabilita WHERE data_chiusura IS NULL AND cancella_note IS NULL) inCorso,
(SELECT COUNT(*) FROM ... [query da_analizzare]) daAnalizzare,
(SELECT COUNT(*) FROM test_germinabilita WHERE cancella_note IS NOT NULL) cancellate,
(SELECT COUNT(*) FROM test_germinabilita WHERE data_chiusura IS NOT NULL AND cancella_note IS NULL) chiuse;
Usato per badge navbar + dashboard.
5. Roadmap 8 sprint — dettagli operativi
Ogni sprint è descritto con: scope, giorni, files toccati, deliverable verificabile.
Sprint 0 — Fondamenta DB (3 giorni)
Giorno 1 — Migration SQL
- File:
sql/migrations/2026-04-XX_germinabilita_schema.sql - Esegui tutti gli ALTER e CREATE descritti in §3
- Popola tipologie_prova (3 righe) e tipologie_seme (9 righe)
- Verifica:
SHOW COLUMNS FROM test_germinabilita→ deve mostrare i 30+ nuovi campi - Verifica:
SELECT COUNT(*) FROM tipologie_prova= 3
Giorno 2 — Registrazione entità in api.php
- File:
api.php - Aggiungi in
$ENTITY_MAP:'test_germinabilita_conte' => ['table' => 'test_germinabilita_conte', 'pk' => 'id'], 'tipologie_prova' => ['table' => 'tipologie_prova', 'pk' => 'id'], 'tipologie_seme' => ['table' => 'tipologie_seme', 'pk' => 'id'], 'specie_calendario_analisi' => ['table' => 'specie_calendario_analisi', 'pk' => 'id'], - Aggiungi in
getNumFields('test_germinabilita')i nuovi campi numerici - Test:
curl api.php?entity=tipologie_seme→ ritorna array 9 elementi
Giorno 3 — Skeleton api_germinabilita.php
- File:
api_germinabilita.php - Implementa almeno:
in_corso,da_analizzare,stats,nuova_analisi,genera_ft - Le azioni più complesse (
chiudi,ricalcola,certificato) per ora ritornano "TODO" - Test:
curl api_germinabilita.php?action=stats→{inCorso:0, daAnalizzare:0, cancellate:0, chiuse:0}
- Schema DB esteso in produzione
- 15 entità disponibili via CRUD in api.php
- 9 azioni custom disponibili in api_germinabilita.php
- Documento changelog su cosa è stato fatto
Sprint 1 — Anagrafiche (4 giorni)
Giorno 1 — Form edit Specie con matrice calendario
- File:
germinabilita.html, tab "🌱 Nuovo modulo" - Aggiungi sotto-tab "🌿 Specie"
- Form edit specie con 8 campi base (italiano, inglese, latino, codice, giorni1, giorniT, UR, note)
- Matrice 12 righe (mesi) × 9 colonne (tipologie seme) con checkbox
- JS: al save calcola diff tra matrice attuale e stato DB, genera array di INSERT e DELETE mirati su
specie_calendario_analisi - Test: crea specie "Test", spunta 3 celle (Gen/Natura, Mag/Selezionato, Ott/Calibrato), salva, ricarica la pagina, verifica spunte preservate
Giorno 2 — Lista e edit Varietà
- Sotto-tab "🌱 Varietà"
- Filtro specie (dropdown)
- Al cambio specie, lista varietà si aggiorna via AJAX
- Form nuova varietà: nome + codice + specie (dropdown)
- Test: creo 3 varietà sotto "Pomodoro", le vedo nella lista, le filtro per specie, apro edit
Giorno 3 — Lista e edit Produttori
- Sotto-tab "🏭 Produttori"
- Riusa il modulo
soggetti.htmlesistente ma filtra solo produttori - Aggiungi il campo
codiceal form produttore - Endpoint inline "+Aggiungi produttore" (POST AJAX): restituisce JSON con l'id nuovo + il nome formattato
- Test: aggiungo produttore "Test Producer" con codice "TP", lo vedo nella lista, lo modifico
Giorno 4 — Test end-to-end anagrafiche
- Scenario completo: creo "Pomodoro" con matrice, "San Marzano" sotto Pomodoro, "Az. Test" produttore
- Verifica nel DB: 1 row in categories, 1 in seeds, 1 in produttori, 3 in specie_calendario_analisi
Sprint 2 — Lotti (3 giorni)
Giorno 1 — Form crea lotto
- Sotto-tab "📦 Lotti" → bottone "+ Nuovo lotto"
- Form con: specie (dropdown) → varietà (cascata AJAX) → produttore (dropdown + modal inline) → kg → data arrivo → codice lotto (text)
- Check duplicato: se esiste lotto con stesso
num, mostra warning + checkbox "Forza creazione duplicato" - Test: crea 5 lotti diversi
Giorno 2 — Lista lotti con filtri
- Tabella con colonne: num, specie, varietà, produttore, kg, dataArrivo, esaurito, azioni
- Filtri: specie, varietà, produttore, stato (esaurito)
- Ordinabile per colonna
- Paginazione: 50 righe/pagina
Giorno 3 — Bulk "Setta come esaurito"
- Checkbox per selezionare righe multiple
- Bottone "Setta esauriti" → POST con array di IDs
- Conferma: "Stai per marcare N lotti come esauriti. Proseguire?"
- Test: seleziono 3 lotti, bulk esaurito, verifica
esaurito=1, data_esaurito=CURDATE()
Sprint 3 — Analisi core (5 giorni)
Il cuore del modulo. Sprint più lungo perché replica l'update.jsp 739 righe di Semiorto.
Giorno 1 — Pagina "Nuova analisi"
- Lista lotti non analizzati (ultima analisi chiusa > N giorni fa, o mai analizzati)
- Filtro per specie
- Click su lotto → POST
api_germinabilita.php?action=nuova_analisi&lot_id=X - Redirect al form analisi con id ricevuto
Giorno 2 — Form analisi: sezione dati prova
- 18 campi in griglia: FT (readonly), lotto (readonly), tipologia seme, prova, categoria, replica, calibratura, dataArrivo, dataInizio, UR, giorniTot, semePuro, materialeInerte, altriSemi, osservazioniPurezza, osservazioni, riferimento (4 sub-campi)
- Validazione client-side + submit senza ricarica
- Test: compilo tutti i campi, salvo, ricarico pagina, vedo persistenza
Giorno 3 — Form analisi: sezione conte A/B/C/D (il difficile)
- Tabella dinamica: 1 riga per ripetizione
- Per ogni riga: lettera (auto), giorni_1, 5 input numerici germ/duri/freschi/anormali/morti → giorni_2 + 5 input → vigore1 + vigore2
- Pulsante "+ Conta" aggiunge riga con lettera prossima (A→B→C→D→E)
- Pulsante "× Elimina" rimuove riga (confirm se era salvata)
- JS onBlur ricalcola totali + mostra germinabilità stimata in header
- Salva conte → POST a
api.php?entity=test_germinabilita_contebulk
Giorno 4 — Form analisi: chiusura
- Bottone "Chiudi analisi" grande, in fondo
- Click → validation (tutte le conte hanno giorni_2, richiesti tecnico/risultato/scostamento/vigore)
- Se OK apri modal: ricalcolo germinabilità live + campi chiusura (risultato POS/NEG, scostamento, vigore manuale opzionale, tecnico)
- Conferma → POST
api_germinabilita.php?action=chiudi_analisi&id=X - Redirect alla lista "In Corso" (adesso l'analisi non c'è più)
Giorno 5 — Endpoint backend + test end-to-end
- Implementa
chiudi_analisi,ricalcola_germinabilitasecondo la formula di §5 di calcoli - Unit test PHP: data 4 conte mock, chiama la formula, verifica germinabilità = somma germinati medi normalizzata
- Test end-to-end: creo lotto → nuova analisi → compilo dati → aggiungo 4 conte con valori noti → chiudo → verifica germinabilità = valore atteso
Sprint 4 — Workflow & dashboard (3 giorni)
Giorno 1 — Lista "In Corso"
- Sotto-tab "📋 In Corso"
- Tabella: FT, specie, varietà, lotto, produttore, prossima operazione (calcolata), azioni
- "Prossima operazione": se 0 conte → "1ª conta il {data_inizio + giorni_1_conta}", altrimenti → "2ª conta il {data_inizio + giorni_tot_conta}"
- Badge navbar: count analisi in corso (GET stats ogni 30 sec con setInterval)
Giorno 2 — Lista "Da Analizzare"
- Sotto-tab "⏰ Da Analizzare"
- Esegue la query §4.2 (la complessa)
- Colonne: lotto, specie, varietà, produttore, ultima analisi, giorni trascorsi
- Azioni: "+ Avvia analisi" → redirect a nuova analisi su quel lotto; "Setta esaurito" se lotto finito
- Badge navbar con count
Giorno 3 — Cerca + Cancellate
- Sotto-tab "🔍 Cerca": form con 8 filtri (specie, varietà, lotto, FT, tipologia seme, prova, anni, chiusa sì/no), tabella risultati ordinata per specie→varietà→data
- Sotto-tab "🗑 Cancellate" (visibile solo ADMIN): lista con motivo cancellazione, azioni "Ripristina" e "Elimina definitivamente"
Sprint 5 — Stampe PDF (4 giorni)
Giorno 1 — Setup engine PDF
- Verifica se TopSeed ha già mpdf o dompdf in uso
- Se no: installa dompdf via
composer require dompdf/dompdf(o copia in/vendor/dompdf/) - Test: genera un PDF "Hello World" via endpoint
Giorno 2 — Template certificato HTML
- File:
templates/certificato_germinabilita.html - Layout professionale: intestazione azienda, logo, titolo "Certificato di Analisi di Germinabilità"
- Tabella metadati: FT, data, lotto, specie (nome inglese + latino in corsivo), varietà, produttore, provenienza
- Tabella risultati: germinabilità, materiale inerte, semi puri, altri semi, vigore, scostamento
- Placeholder stile
{{nome_specie_en}},{{ft}}, ecc. - Firma tecnico in fondo
Giorno 3 — Endpoint certificato
- Implementa
api_germinabilita.php?action=certificato&id=X - Query JOIN per raccogliere tutti i dati necessari (20 placeholder)
- Sostituzione placeholder con regex
- DOMPDF genera PDF
- Response: Content-Type application/pdf, filename
Certificato_Germinabilita_{lotto}_{FT}.pdf - Test: apro un'analisi chiusa in TopSeed, clicco "Stampa certificato", confronto con certificato Semiorto (se disponibile)
Giorno 4 — Scheda controllo + editor template
- Template
templates/scheda_controllo.html: più dettagliato, include tabella di tutte le conte A/B/C/D - Endpoint
?action=scheda - Bonus (se tempo): aggiungi sezione in
config.html→ "Template stampe germinabilità" dove admin può editare i 2 template HTML live
Sprint 6 — Migrazione dati (3 giorni)
Giorno 1 — Script migrate_semiorto.php
- File:
scripts/migrate_semiorto.php - Implementa 9 step in sequenza (specie, varietà, produttori, tecnici, utenti, lotti, analisi, conte, calendario). Vedi migrazione per pseudo-codice.
- Esegue tutto in una transazione unica con ROLLBACK automatico
- Logging dettagliato: ogni 1000 record stampa "progresso: X/Y"
- Dry-run mode: flag
--dryesegue tutto ma fa ROLLBACK finale
Giorno 2 — Dry-run su staging
- Copia DB TopSeed produzione in DB
topseed_db_staging - Esegui
php migrate_semiorto.php --drypuntando a staging - Tempo atteso: ~10 minuti
- Verifica log: nessun errore, tutti i counts attesi
- Rimuovi --dry, esegui reale su staging
- Esegui query di validazione (§12 di migrazione)
- Spot check manuale: 50 analisi random, confronto campo-per-campo con SQL parallelo su Semiorto
Giorno 3 — Fix + secondo dry-run
- Se ci sono discrepanze, fix script
- Secondo dry-run su staging pulito
- Quando validazione passa al 100% → pronto per taglio finale
Sprint 7 — Go-live (2 giorni)
Giorno 1 — Switch utenti
- Backup completo DB TopSeed pre-migrazione
- Esegui
migrate_semiorto.phpin produzione (finestra low-traffic, es. sabato mattina) - Verifica counts e validazione
- Invia email a tutti gli utenti Semiorto: "Da oggi il nuovo sistema è attivo. Credenziali uguali, alla prima login vi chiederà di cambiare password."
- Comunica al laboratorio il cambio operativo
Giorno 2 — Safety net + monitoring
- Mantieni tab "📜 Germinabilità OLD" per consultazione (read-only)
- Monitoring quotidiano: count analisi TopSeed vs count Semiorto AWS → se divergono (qualcuno usa ancora Semiorto) → alert
- Check login: quanti utenti si sono loggati in TopSeed nelle 24h post-migrazione
- Dopo 30 giorni di safety net: spegni container Tomcat locale, mantieni solo dump SQL legacy
6. Scelte tecniche con motivazioni puntuali
6.1 Mesi come INT invece di FK a tabella
Semiorto ha semiorto_analisi_ripetizione con 12 righe per i mesi. È over-engineering. Noi usiamo INT 1-12 direttamente in specie_calendario_analisi.mese. Vantaggi:
- Una JOIN in meno nelle query (le query sui mesi sono tante, ogni JOIN conta)
- Campo autoesplicativo (12 = dicembre, non serve decodifica)
- Nomi dei mesi sono i18n: se l'applicazione deve uscire in inglese,
MONTH_NAMES[11]='December'è una variabile JS invece di un campo DB da tradurre
6.2 Mappatura ruoli: ADMIN / GENETISTA / MAGAZZINIERE
Semiorto ha Amministrazione / Laboratorio / Magazzino. TopSeed ha già ADMIN / GENETISTA / MAGAZZINIERE. Usiamo i nomi TopSeed. Conversione:
- Amministrazione → ADMIN (coincidenza semantica)
- Laboratorio → GENETISTA (TopSeed ha questo ruolo per i test genetici/germinabilità)
- Magazzino → MAGAZZINIERE (coincidenza)
- Utente Semiorto con 2+ ruoli → il ruolo più alto (di solito ADMIN)
6.3 Stampe: PDF invece di DOCX
Motivazioni:
- PDF è immutabile: il cliente non può modificare un certificato. DOCX sì (con Word aperto).
- Template HTML editabile: con HTML + CSS, l'admin può modificare il layout direttamente da browser via pagina config. Con DOCX serve MS Word, competenze grafiche, risorse tecniche.
- Niente dipendenze MS: docx4j richiede java + licenze MS Office nel flusso editing. dompdf è zero-dep PHP.
- Mobile-friendly: PDF si apre nativamente su qualsiasi dispositivo. DOCX richiede Word o app compatibili.
Perdita: perdiamo la possibilità di editare il certificato prima di inviarlo. Mitigazione: se il cliente chiede modifiche, il tecnico può cambiare i dati in TopSeed e rigenerare.
6.4 FT generation: SELECT FOR UPDATE invece di max+1 lazy
Semiorto usa:
-- Race condition possibile
max_ft = SELECT MAX(ft) FROM ... WHERE year = Y
new_ft = max_ft + 1
Se due tecnici creano analisi contemporaneamente, potrebbero ottenere lo stesso FT. Improbabile con 4 tecnici ma non impossibile.
Soluzione nostra:
BEGIN;
SELECT MAX(...) FOR UPDATE; -- lock pessimistico
INSERT ... FT = max+1;
COMMIT;
Penalità prestazionale trascurabile con 4 concurrent users. Robustezza totale.
6.5 cancella_note VARCHAR invece di is_deleted BOOL
Soft-delete con motivo = audit trail migliore. L'admin può capire perché un'analisi è stata cancellata senza query log separati.
6.6 Non implementiamo generaLotto
La funzione LottoServiceImpl.generaLotto() di Semiorto genera un codice auto-calcolato per il lotto. Ma nessun controller la invoca — l'operatore inserisce il codice manualmente. È dead code. La saltiamo.
Se in futuro serve: aggiungeremo un bottone "Suggerisci codice" nel form lotto che chiama endpoint dedicato con logica nostra (migliorata rispetto a quella originale bug-prone).
7. Rischi con mitigazioni puntuali
R1 — Schema test_germinabilita già ha dati di test
Probabilità: bassa (ho controllato, tabella quasi vuota)
Impatto: potenziali conflitti durante ALTER se ci sono dati con valori strani
Mitigazione: backup pre-ALTER, rollback pronto. Se ci sono < 10 record di test, cancellarli prima.
R2 — Template DOCX certificato non ottenibile
Probabilità: media (Diego potrebbe non averlo conservato o non volerlo condividere)
Impatto: non abbiamo il layout originale da replicare
Mitigazione: ricostruire da zero. Chiedi al cliente un certificato Semiorto stampato in PDF. Riproduci il layout in HTML. Mostralo al cliente per validazione. Itera.
R3 — Formula calcolo germinabilità differisce
Probabilità: bassa (abbiamo il codice Java di riferimento)
Impatto: dopo migrazione, i nuovi calcoli potrebbero dare valori diversi dai legacy
Mitigazione: test su 100 analisi legacy. Usando le conte come input, ricalcolare con formula nostra, confrontare con germinabilita stored. Tolleranza: differenza ≤ 1 (arrotondamenti diversi).
R4 — Utenti SHA-1 non riescono a loggarsi
Probabilità: media (ShaPasswordEncoder ha configurazioni specifiche, un detail diverso rompe tutto)
Impatto: 5 utenti bloccati al giorno del go-live
Mitigazione: testare il flusso legacy_sha_hash con utente di staging PRIMA del go-live. Se fallisce → forzare reset password per tutti via email automatica al day-1.
R5 — Query "Da Analizzare" troppo pesante su 62K analisi
Probabilità: bassa (la query Semiorto gira in produzione da anni con 62K record senza problemi)
Impatto: badge navbar lento, lista impiega > 2 secondi
Mitigazione: indici su test_germinabilita.data_chiusura, lots.esaurito, specie_calendario_analisi.mese. Se ancora lenta → cache il risultato per 1 ora (la query dipende da MONTH(NOW()) che cambia ~1 volta al mese).
R6 — Race condition FT
Vedi §6.4. Mitigazione: SELECT FOR UPDATE.
R7 — Utenti confusi dalla UI nuova
Probabilità: media (la UI Bootstrap 5 di TopSeed è molto diversa da Bootstrap 3 di Diego)
Impatto: resistenza iniziale, produttività bassa nei primi giorni
Mitigazione: sessione di training di 1 ora via screen-share con ogni utente. Manuale utente come pagina del dossier. Safety net 30 giorni dove possono consultare Semiorto se serve conferma.
8. Decisioni da prendere prima di partire
- OK alla roadmap generale (8 sprint, ~7 settimane)? Se hai vincoli di timeline comprimere/allentare?
- PDF engine: dompdf (semplice, 100% PHP) o mpdf (più feature, più CSS support)? → io propongo dompdf perché è più leggero, ma se TopSeed ne usa già uno è meglio adottarlo.
- Ruoli: uso ADMIN/GENETISTA/MAGAZZINIERE TopSeed esistenti? → Sì, è la mia raccomandazione per coerenza.
- Migrazione Big-Bang o progressiva? → Big-Bang con dry-run. Scegliamo un sabato, backup pre-migrazione, eseguiamo script, validiamo, notifichiamo utenti lunedì mattina.
- Contattiamo Diego per template DOCX e SMTP? Decisione commerciale: se il rapporto con lui è ancora buono, chiedere. Se no, ricostruiamo tutto.
- Azienda single vs multi-azienda? Semiorto gestisce 1 azienda. TopSeed lo supporta multi. Nel Sprint 1-7 → SINGOLA (semplicità). In fase 2 post-go-live → aggiungiamo multi-azienda se serve.
- Ripristino analisi cancellate: chi può farlo? Solo ADMIN? O anche GENETISTA che l'ha cancellata? → io propongo ADMIN only (più sicuro).
- Eliminazione definitiva: in Semiorto l'ADMIN può farlo. In TopSeed aggiungiamo una finestra di conferma tipo "Scrivi DELETE per confermare". O togliamo proprio la feature (chi la usa?). → io propongo di mantenerla ma richiedendo doppia conferma.
- Template certificato: 1 solo template o multi-azienda già nel v1? → nel v1 uno solo, chiamato SEMIORTO. Se poi serve multi, aggiungiamo un
azienda_idin config. - Login post-migrazione: forzare reset pwd a tutti (più sicuro ma più attrito) o provare prima SHA-1 match (più smooth ma più rischioso)? → io propongo SHA-1 match con fallback reset.
Parto con Sprint 0 giorno 1: deploy della migration SQL in produzione, popolamento lookup tables, registrazione entità in api.php, skeleton di api_germinabilita.php. Tempo stimato: 3-4 ore (parte di oggi).
Fine Sprint 0: entro fine settimana.