Flussi operativi

Il ciclo di vita di un'analisi dall'arrivo del seme fino al certificato

Flusso completo ad alto livello

┌─────────────────────┐
│  ARRIVO SEME        │  Magazzino (o fornitore esterno) consegna seme
│  ─────────────────  │
│  Magazziniere crea  │
│  LOTTO              │  /lottos/lotto/new (specie, varietà, produttore, kg, data arrivo)
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  RICHIESTA ANALISI  │  Tecnico laboratorio apre nuova analisi
│  ─────────────────  │
│  FT auto-generato   │  /analisis/lotto/{id}/new (compilati lotto, dataArrivoLab)
│  Compila parametri  │  Tipologia seme, Prova, Categoria, UR, Totale giorni
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  CONTE GIORNALIERE  │  Per ogni ripetizione (A,B,C,D) e giorno
│  ─────────────────  │
│  Giorno 5  → lettura 1 (germinati, duri, freschi, anormali, morti, vigore)
│  Giorno 10 → lettura 2 (idem)
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  CHIUSURA ANALISI   │
│  ─────────────────  │
│  Calcola germ media │  germinato1_medio + germinato2_medio (normalizzato a 100)
│  Tecnico imposta    │  Risultato (POS/NEG), Scostamento, Vigore finale
│  data = oggi        │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  STAMPA             │
│  ─────────────────  │
│  /scheda            │  PDF scheda di controllo interna
│  /certificato       │  DOCX certificato per cliente (template con $$_$$)
└─────────────────────┘

Flussi secondari:
  • Lotto esaurito → /lottos/settaEsaurito (bulk) o /analisis/lottoEsaurito/{id}
  • Cancellazione analisi → soft delete (cancella = motivo)
  • Ripristino cancellata → /amministrazione/analisis/cancellate

Diagramma di stato dell'analisi

                  [nessuna entity]
                         │
                         │ POST /analisis/lotto/{id}/new
                         ▼
                  ┌──────────────────┐
                  │  DA ANALIZZARE   │     data = NULL
                  │  (iniziale)      │     cancella = NULL
                  └────────┬─────────┘
                           │
                  Aggiunta di conte
                           │
                           ▼
                  ┌──────────────────┐
                  │  IN CORSO        │     data = NULL
                  │                  │     cancella = NULL
                  │                  │     lotto.esaurito = false
                  └────────┬─────────┘
                           │
                     ┌─────┴─────┐
         "CHIUDI_ANALISI"        │ "Cancella"
                     ▼           ▼
           ┌───────────────┐  ┌──────────────────┐
           │   CHIUSA      │  │   CANCELLATA     │
           │  data = oggi  │  │  cancella = "..." │  (soft)
           │  risultato    │  │  data ANY         │
           │  POSITIVO/NEG │  └───────┬──────────┘
           └───────────────┘          │
                                "Cancellazione definitiva"
                                      │
                                      ▼
                                 [DELETED hard]

Le 3 query "magiche"

Query "In Corso"

-- Criteria Hibernate equivalent:
SELECT sa.* FROM semiorto_analisi sa
LEFT JOIN semiorto_lotto sl ON sa.id_lotto = sl.id_lotto
WHERE sa.data IS NULL
  AND sa.cancella IS NULL
  AND (sl.esaurito IS NULL OR sl.esaurito = FALSE);

Risultato: analisi aperte, non cancellate, su lotti attivi.

Query "Da Analizzare"

Questa è la più complessa. Risponde a: "Quali lotti dovrebbero essere rianalizzati questo mese ma non lo sono stati?"

-- Pseudo-codice dal sorgente Java:
SET @month = MONTH(NOW());                       -- 1..12
SET @first_of_month = ;
SET @day_of_month = ;

SELECT sa.*
FROM semiorto_analisi sa
JOIN (
  -- subquery: ultima analisi chiusa per ogni lotto attivo
  -- che corrisponde al calendario del mese corrente
  SELECT MAX(a.id) id, l.id_lotto, s.giorniTConta
  FROM semiorto_analisi a
  JOIN semiorto_lotto l ON a.id_lotto = l.id_lotto
  JOIN semiorto_varieta v ON l.varieta = v.id
  JOIN semiorto_specie s ON v.specie = s.id
  JOIN semiorto_map_specie_analisi_ripetizione_tipologia_seme m
       ON s.id = m.id_specie
  WHERE l.esaurito = FALSE
    AND m.id_analisi_ripetizione = @month  -- mese corrente
  GROUP BY l.id_lotto
) q ON sa.id = q.id
WHERE sa.data IS NOT NULL
  AND DATE_ADD(sa.data, INTERVAL (@day_of_month - q.giorniTConta) DAY)
      < @first_of_month
ORDER BY sa.id;

Decodifica step-by-step

  1. Per ogni lotto attivo (esaurito=false) prendi l'ID dell'ultima analisi chiusa
  2. Filtra per mese: solo se la combinazione (specie del lotto, mese corrente) è presente nella tabella calendario map. In altre parole: solo se per quella specie è previsto un check in questo mese.
  3. Calcola "scadenza": l'analisi era "valida" per giorniTConta (tipicamente 10-14 giorni). Dopo tanti giorni deve essere rifatta.
  4. Test temporale: l'ultima analisi è "in scadenza" se la sua data di chiusura + (giorni nel mese precedente − giorniTConta) < primo giorno del mese corrente.
  5. Risultato: tutti i lotti che "dovrebbero" avere un'analisi nuova questo mese ma non ce l'hanno.
Esempio concreto

Oggi è 20 Aprile 2026. Mese = 4.

  1. Subquery: trova ultima analisi chiusa per ogni lotto non esaurito. Filtra solo lotti di specie che prevedono test ad Aprile (configurato nella tabella map).
  2. Es.: lotto "VSR1710/TS" di Peperone, ultima analisi chiusa il 10/02/2026 con giorniTConta=10.
  3. Check temporale: 2026-02-10 + (31 - 10) giorni = 2026-03-03, che è minore di 2026-04-01 → ✅ appare in "Da Analizzare".

Query "Cancellate"

SELECT * FROM semiorto_analisi
WHERE cancella IS NOT NULL
ORDER BY specie.italiano, varieta.nome, lotto.id, id DESC;

Generazione FT (fattura tecnica)

Ogni analisi ha un identificativo ft nel formato NNN/YY (es. 3561/20).

anno = format(now(), "yy")        // 20, 21, 22, ..., 26
ultimoFT = query "SELECT ft FROM semiorto_analisi
                  WHERE ft LIKE '%/" + anno + "'
                  ORDER BY id DESC LIMIT 1"

if (ultimoFT == null) {
    numero = 1
} else {
    numero = int(ultimoFT.split("/")[0]) + 1
}
ft = numero + "/" + anno    // es. "3561/20"

Il numero è globale sull'anno, non resettato per specie o laboratorio.

Flusso stampa scheda di controllo (PDF)

  1. Utente clicca "Stampa Scheda" sul dettaglio analisi
  2. Browser fa GET a /analisis/analisi/{id}/{azienda}/scheda
  3. Controller chiama StampaSchedaControllo.schedaControllo(…) → classe custom che costruisce PDF
  4. Response: Content-Type: application/pdf, body = bytes PDF
  5. Browser mostra o scarica

Flusso stampa certificato (DOCX)

  1. GET /analisis/analisi/{id}/{azienda}/certificato
  2. Controller popola mappa substitutionData con 20 chiavi (NOME_SPECIE, FT, DATA_ARRIVO, ecc. — vedi Calcoli)
  3. Legge template WEB-INF/TEMPLATES_DIRECTORY/modello-certificato-{azienda}.docx
  4. DocxManipulator.generateAndSendDocx() sostituisce placeholder $$_NOME_SPECIE_$$ ecc. con i valori
  5. Response: Content-Type: application/vnd.openxmlformats-officedocument..., filename Quality-Certificate-{lotto}-YYYY/MM/YY.docx
⚠ Template non presente nel dump

La cartella TEMPLATES_DIRECTORY/ è vuota nel nostro WAR ricevuto. Per la migrazione TopSeed serve chiedere a Diego il file .docx aggiornato o ricostruirlo dai certificati già emessi (scan PDF).

Flusso login + routing iniziale

GET / (root)
  │
  ▼
Spring Security: utente non autenticato?
  ├── SÌ → redirect a /accesso/login → form → POST /j_spring_security_check
  │         └── OK → set JSESSIONID cookie → redirect a /
  │
  └── NO → DefaultController.index():
            │
            ▼
       Query utente.cambiaPassword?
            │
            ├── SÌ → redirect a /utente/cambioPassword (forzato)
            │
            └── NO → costruisce menu via MenuService.menu(utente)
                     │
                     ▼
                salva menu + utente in session
                     │
                     ▼
                redirect alla PRIMA voce menu abilitata per l'utente:
                  • Amministrazione → /amministrazione/utenti
                  • Laboratorio     → /analisis/inCorso
                  • Magazzino       → /analisis/inCorso
                  (fallback: /index)

Avvisi in navbar (AJAX badge)

Ogni pagina include template/menu.jsp che alla fine esegue:

$.getJSON("/analisis/avvisi/json", function(data) {
    $('#item-2').html(... + 'inCorso badge: ' + data.inCorso + '...');
    $('#item-8').html(... + 'daAnalizzare badge: ' + data.daAnalizzare + '...');
});

Endpoint backend:

@RequestMapping("/analisis/avvisi/json")
@ResponseBody
public Map<String,Integer> avvisi() {
    return Map.of(
        "inCorso", analisiService.inCorsoNumber(),
        "daAnalizzare", analisiService.daAnalizzareNumber()
    );
}

Questo spiega i badge rossi che appaiono di fianco alle voci "Analisi" e "Da Analizzare" nella navbar.