Salta al contenuto principale

12 - Sondaggi e Questionari in Tempo Reale

Sondaggi e Questionari: Due Sistemi Complementari per Decisioni Collettive Organizzare un evento significa prendere decine di decisioni condivise: "Dove...

Cosa ne pensi di questo articolo?

12 - Sondaggi e Questionari in Tempo Reale
Play the EventFoto:panumas nikhomkhaiPexels

In Breve

Sondaggi e Questionari: Due Sistemi Complementari per Decisioni Collettive Organizzare un evento significa prendere decine di decisioni condivise: "Dove...

Sondaggi e Questionari: Due Sistemi Complementari per Decisioni Collettive

Organizzare un evento significa prendere decine di decisioni condivise: "Dove andiamo? Cosa mangiamo? Quale data preferiamo?". In Play The Event queste domande trovano risposta attraverso due sistemi distinti ma complementari: i Sondaggi per votazioni rapide su opzioni predefinite e i Questionari per raccogliere feedback strutturati con domande di diverso tipo. Insieme, coprono ogni esigenza di raccolta opinioni nella pianificazione di un evento.

Cosa Troverai in Questo Articolo

  • La macchina a stati del sondaggio (BOZZA, ATTIVO, CHIUSO, ANNULLATO)
  • Risposte SINGOLA e MULTIPLA con max opzioni selezionabili configurabile
  • Votazione anonima e sondaggi pubblici con codice di condivisione UUID
  • Template riutilizzabili con deep copy di opzioni
  • Storico vincitori con motivazione obbligatoria per ogni cambio
  • Sondaggi per entità (LUOGO, DATA, MENU, ATTIVITA, CUSTOM)
  • Sistema Questionari con domande tipizzate e compilazioni tracciate
  • Visualizzazione risultati in tempo reale

Il Sondaggio come Aggregate Root

Nel domain model di Play The Event, il Sondaggio è un Aggregate Root ricco di comportamento. Non è un semplice contenitore di opzioni: gestisce il proprio ciclo di vita, valida le transizioni di stato, controlla la votazione e mantiene lo storico completo delle decisioni prese.

Modello Dati: Sondaggio (campi principali)
@Entity
@Table(name = "sondaggi")
public class Sondaggio {
    private Long id;
    private Evento evento;
    private TipoEntitaSondaggio tipoEntita;  // LUOGO, DATA, MENU...
    private Long entitaId;
    private String titolo;
    private String descrizione;
    private TipoRispostaSondaggio tipoRisposta; // SINGOLA o MULTIPLA
    private StatoSondaggio stato;               // BOZZA, ATTIVO, CHIUSO, ANNULLATO
    private TipoSondaggio tipo;                 // EVENTO o TEMPLATE
    private Long templateOrigineId;
    private LocalDateTime dataScadenza;
    private Boolean anonimo;
    private Boolean permettiModificaVoto;
    private Boolean mostraRisultatiPrimaVoto;
    private Integer maxOpzioniSelezionabili;
    private Boolean pubblico;
    private String codiceCondivisione;          // UUID per sharing
    private Set<OpzioneSondaggio> opzioni;
    private OpzioneSondaggio opzioneVincitrice;
    private Set<StoricoVincitoreSondaggio> storicoVincitori;
}

Ogni sondaggio è collegato a un evento, ha un tipo di entità che indica a cosa si riferisce (luogo, data, menu, attività), e gestisce internamente le proprie opzioni, risposte e lo storico dei vincitori. Il pattern Aggregate Root garantisce che tutte le invarianti di business siano rispettate in ogni momento.

Macchina a Stati del Sondaggio

Il ciclo di vita di un sondaggio segue una macchina a stati ben definita con quattro stati possibili e transizioni controllate. Ogni transizione è validata dal domain model stesso, rendendo impossibile raggiungere stati invalidi.

StatoSondaggio e Transizioni
public enum StatoSondaggio {
    BOZZA("Bozza"),         // Creazione e configurazione
    ATTIVO("Attivo"),       // Aperto alle votazioni
    CHIUSO("Chiuso"),       // Votazioni terminate
    ANNULLATO("Annullato"); // Sondaggio invalidato
}

Le transizioni consentite sono:

  • BOZZA → ATTIVO (pubblica()): richiede almeno 2 opzioni
  • ATTIVO → CHIUSO (chiudi()): termina le votazioni
  • ATTIVO → ANNULLATO (annulla()): invalida il sondaggio
  • CHIUSO → ATTIVO (riapri()): riapre le votazioni
Metodi di Transizione con Validazione
public void pubblica() {
    if (this.stato != StatoSondaggio.BOZZA) {
        throw new IllegalStateException(
            "Solo sondaggi in bozza possono essere pubblicati");
    }
    if (this.opzioni.size() < 2) {
        throw new IllegalStateException(
            "Il sondaggio deve avere almeno 2 opzioni");
    }
    this.stato = StatoSondaggio.ATTIVO;
}

public void riapri() {
    if (this.stato != StatoSondaggio.CHIUSO) {
        throw new IllegalStateException(
            "Solo sondaggi chiusi possono essere riaperti");
    }
    this.stato = StatoSondaggio.ATTIVO;
}

Perché la Riapertura?

La transizione CHIUSO → ATTIVO è una scelta progettuale importante. Nella vita reale le decisioni cambiano: un luogo diventa non disponibile, arrivano nuovi partecipanti che vogliono votare, o semplicemente servono più voti per prendere una decisione. Invece di creare un nuovo sondaggio e perdere i voti esistenti, la riapertura permette di continuare la raccolta in modo trasparente.

Tipi di Risposta: SINGOLA e MULTIPLA

Il sistema supporta due modalità di votazione che coprono gli scenari più comuni nella pianificazione di eventi.

TipoRispostaSondaggio
public enum TipoRispostaSondaggio {
    SINGOLA("Singola"),    // Radio button - una sola scelta
    MULTIPLA("Multipla");  // Checkbox - più' opzioni selezionabili
}

Nel caso di risposta SINGOLA, il campo maxOpzioniSelezionabili viene impostato automaticamente a 1. Per la risposta MULTIPLA, il valore predefinito è 3 ma è configurabile dall'organizzatore. Questo permette scenari come "scegli le tue 3 attività preferite tra 10 proposte".

Configurazione Automatica nel Factory Method
public static Sondaggio crea(Evento evento, User creatoDa,
        TipoEntitaSondaggio tipoEntita, String titolo,
        TipoRispostaSondaggio tipoRisposta) {
    Sondaggio s = new Sondaggio();
    // ... inizializzazione campi ...
    s.maxOpzioniSelezionabili =
        tipoRisposta == TipoRispostaSondaggio.SINGOLA ? 1 : 3;
    s.permettiModificaVoto = true;
    s.mostraRisultatiPrimaVoto = false;
    return s;
}

Votazione Anonima

Ogni sondaggio può essere configurato come anonimo. Quando l'anonimato è attivo, le risposte vengono registrate senza associarle all'utente votante, garantendo la privacy nelle decisioni sensibili. Il campo anonimo è impostato a false per default: l'organizzatore deve abilitarlo esplicitamente.

Le risposte anonime utilizzano il factory method creaAnonima() che non richiede né un utente né un'email. Il tracciamento per prevenire voti doppi viene gestito separatamente tramite browser fingerprint.

RispostaSondaggio: Tre Modalità di Creazione
// Voto di un utente registrato
RispostaSondaggio.creaPerUtente(sondaggio, opzione, utente);

// Voto di un esterno via email (con token di modifica)
RispostaSondaggio.creaPerEmail(sondaggio, opzione, email);

// Voto anonimo su sondaggio pubblico
RispostaSondaggio.creaAnonima(sondaggio, opzione);

Sondaggi Pubblici con Codice di Condivisione

Un sondaggio può essere reso pubblico per raccogliere voti anche da persone non registrate sulla piattaforma. Il sistema genera un codice di condivisione UUID univoco che può essere distribuito come link.

Gestione Sondaggio Pubblico
public String rendiPubblico() {
    if (this.stato == StatoSondaggio.BOZZA) {
        throw new IllegalStateException(
            "Pubblica il sondaggio prima di renderlo pubblico");
    }
    if (this.pubblico && this.codiceCondivisione != null) {
        return this.codiceCondivisione; // Gia' pubblico
    }
    this.pubblico = true;
    this.codiceCondivisione = UUID.randomUUID().toString();
    return this.codiceCondivisione;
}

public String rigeneraCodiceCondivisione() {
    // Invalida i link precedenti
    this.codiceCondivisione = UUID.randomUUID().toString();
    return this.codiceCondivisione;
}

L'entità VotoPubblico traccia i voti anonimi sui sondaggi pubblici. Utilizza un fingerprint del browser per prevenire voti duplicati: un vincolo di unicità su (sondaggio_id, fingerprint) garantisce che ogni dispositivo possa votare una sola volta.

VotoPubblico: Anti-Duplicazione
@Entity
@Table(name = "voti_pubblici",
    uniqueConstraints = @UniqueConstraint(
        columnNames = {"sondaggio_id", "fingerprint"}))
public class VotoPubblico {
    private Long id;
    private Sondaggio sondaggio;
    private String fingerprint;   // Hash del browser
    private Instant votatoIl;
}

Flusso del Sondaggio Pubblico

  • L'organizzatore crea e pubblica il sondaggio (stato ATTIVO)
  • Invoca rendiPubblico() per generare il codice UUID
  • Il link viene condiviso via messaggio, email o social
  • Gli esterni votano senza registrazione, tracciati via fingerprint
  • L'organizzatore può rigenerare il codice per invalidare i link precedenti
  • Può rendere il sondaggio privato e poi ri-abilitarlo mantenendo il codice

Template Riutilizzabili

Il sistema distingue tra sondaggi di tipo EVENTO (collegati a un evento specifico) e sondaggi di tipo TEMPLATE (riutilizzabili). Un template è un sondaggio non collegato ad alcun evento che serve come modello per creare istanze identiche in contesti diversi.

Creazione e Istanziazione Template
// Crea un template riutilizzabile
Sondaggio template = Sondaggio.creaTemplate(
    utente, "Preferenza Location",
    TipoEntitaSondaggio.LUOGO, "Dove facciamo la festa?",
    TipoRispostaSondaggio.SINGOLA);

// Istanzia il template per un evento specifico
Sondaggio istanza = Sondaggio.creaDaTemplate(
    template, evento, organizzatore);
istanza.copiaOpzioniDa(template);

// L'istanza eredita: titolo, descrizione, tipoRisposta,
// configurazione (anonimo, maxSelezionabili, ecc.)
// ma parte sempre in stato BOZZA

La copia delle opzioni è separata dalla creazione dell'istanza per mantenere le relazioni JPA corrette. Ogni opzione viene duplicata tramite OpzioneSondaggio.copiaDa() che crea una nuova entità con gli stessi dati ma senza ID e senza risposte, garantendo un'istanza completamente indipendente dal template.

Storico Vincitori con Audit Trail

Per i sondaggi di tipo LUOGO, il sistema supporta la selezione di un'opzione vincitrice dopo la chiusura delle votazioni. Ogni selezione o cambio di vincitore viene tracciato nell'entità StoricoVincitoreSondaggio, creando un audit trail completo.

StoricoVincitoreSondaggio
@Entity
@Table(name = "storico_vincitori_sondaggio")
public class StoricoVincitoreSondaggio {
    private Long id;
    private Sondaggio sondaggio;
    private OpzioneSondaggio opzionePrecedente; // null se prima selezione
    private OpzioneSondaggio opzioneNuova;
    private String motivazione;                 // obbligatoria per cambi
    private User modificatoDa;
    private Instant modificatoIl;
}

La regola chiave è: la motivazione è obbligatoria quando si cambia un vincitore già selezionato. Per la prima selezione non è richiesta. Questo garantisce trasparenza nelle decisioni del gruppo.

Selezione Vincitore con Validazione
public StoricoVincitoreSondaggio selezionaVincitore(
        Long opzioneId, User selezionatoDa, String motivazione) {
    verificaChiuso();      // Deve essere CHIUSO
    verificaTipoLuogo();   // Solo per sondaggi LUOGO

    OpzioneSondaggio opzione = trovaNelleOpzioni(opzioneId);

    StoricoVincitoreSondaggio storico;
    if (this.opzioneVincitrice != null) {
        // Cambio vincitore: motivazione OBBLIGATORIA
        if (motivazione == null || motivazione.isBlank()) {
            throw new IllegalArgumentException(
                "La motivazione e' obbligatoria per cambiare il vincitore");
        }
        storico = StoricoVincitoreSondaggio.creaCambio(
            this, this.opzioneVincitrice, opzione,
            motivazione, selezionatoDa);
    } else {
        // Prima selezione
        storico = StoricoVincitoreSondaggio.creaPrimaSelezione(
            this, opzione, selezionatoDa);
    }

    this.opzioneVincitrice = opzione;
    this.storicoVincitori.add(storico);
    return storico;
}

Sondaggi per Entità: Oltre la Semplice Domanda

I sondaggi non sono generici: ogni sondaggio è associato a un tipo di entità che ne definisce il contesto e le funzionalità disponibili.

TipoEntitaSondaggio
public enum TipoEntitaSondaggio {
    LUOGO("Luogo"),           // Votare la venue dell'evento
    DATA("Data"),             // Scegliere la data
    MENU("Menu"),             // Preferenze catering
    ATTIVITA("Attività"),      // Attività da organizzare
    CUSTOM("Personalizzato"); // Sondaggio generico
}

Il tipo LUOGO è particolarmente ricco: le opzioni possono essere collegate direttamente a entità Luogo del sistema tramite entitaRiferimentoId, con descrizione e immagine dedicata. Inoltre, solo i sondaggi LUOGO supportano la selezione del vincitore.

OpzioneSondaggio con Riferimento a Entità
@Entity
public class OpzioneSondaggio {
    private Long id;
    private Sondaggio sondaggio;
    private String testo;
    private Long entitaRiferimentoId;  // ID del luogo collegato
    private String descrizione;         // Info aggiuntive
    private String immagineUrl;         // Foto della venue
    private Integer ordine;
    private Set<RispostaSondaggio> risposte;
}

Il factory method dedicato creaPerLuoghi() semplifica la creazione di sondaggi per la scelta della location, pre-impostando il tipo entità a LUOGO e la risposta a SINGOLA.

Opzioni Configurabili

Ogni sondaggio offre tre flag booleani che controllano l'esperienza di voto. Queste configurazioni sono modificabili solo finché il sondaggio è in stato BOZZA.

  • maxOpzioniSelezionabili: limita il numero di scelte nelle risposte multiple. Validato per essere almeno 1.
  • mostraRisultatiPrimaVoto: se true, i partecipanti vedono i risultati parziali prima di votare. Default: false per evitare effetti bandwagon.
  • permettiModificaVoto: se true, i votanti possono cambiare la propria scelta. Default: true.
Configurazione del Sondaggio
public void configura(Boolean anonimo, Boolean permettiModificaVoto,
        Boolean mostraRisultatiPrimaVoto,
        Integer maxOpzioniSelezionabili,
        LocalDateTime dataScadenza) {
    verificaModificabile(); // Solo in stato BOZZA
    if (maxOpzioniSelezionabili != null) {
        if (maxOpzioniSelezionabili < 1) {
            throw new IllegalArgumentException(
                "Deve essere selezionabile almeno un'opzione");
        }
        this.maxOpzioniSelezionabili = maxOpzioniSelezionabili;
    }
    this.dataScadenza = dataScadenza;
    // ... altri campi ...
}

La data di scadenza è opzionale: se impostata, il sondaggio smette automaticamente di accettare voti dopo la scadenza, anche se rimane in stato ATTIVO. Il metodo isVotabile() controlla entrambe le condizioni.

Il Sistema Questionari

Accanto ai sondaggi, Play The Event offre un sistema di Questionari per raccogliere informazioni più strutturate. Mentre il sondaggio è una votazione su opzioni predefinite, il questionario è un modulo con domande di tipo diverso.

Modello Dati: Questionario
@Entity
@Table(name = "questionari")
public class Questionario {
    private Long id;
    private Evento evento;
    private TipoQuestionario tipo;       // TEMPLATE o EVENTO
    private Long templateOrigineId;
    private String titolo;
    private String descrizione;
    private String etichetta;
    private String coloreEtichetta;
    private StatoQuestionario stato;     // BOZZA, PUBBLICATO, CHIUSO, ARCHIVIATO
    private LocalDateTime dataScadenza;
    private Boolean anonimo;
    private Boolean richiediEmail;
    private Boolean richiediNome;
    private Boolean pubblico;
    private String codiceCondivisione;
    private List<DomandaQuestionario> domande;
}

Il ciclo di vita del questionario è simile a quello del sondaggio ma con uno stato in più: BOZZA → PUBBLICATO → CHIUSO → ARCHIVIATO. Lo stato ARCHIVIATO permette di conservare i questionari completati senza ingombrare la vista attiva.

StatoQuestionario
public enum StatoQuestionario {
    BOZZA("Bozza"),           // In fase di creazione
    PUBBLICATO("Pubblicato"), // Aperto alle compilazioni
    CHIUSO("Chiuso"),         // Compilazioni terminate
    ARCHIVIATO("Archiviato"); // Conservato in archivio
}

Tipi di Domanda nel Questionario

Ogni domanda del questionario ha un tipo che determina come viene visualizzata e come vengono raccolte le risposte.

TipoDomanda
public enum TipoDomanda {
    APERTA("Risposta aperta"),           // Testo libero
    SCELTA_SINGOLA("Scelta singola"),    // Radio button
    SCELTA_MULTIPLA("Scelta multipla");  // Checkbox
}

Le domande di tipo APERTA raccolgono testo libero: commenti, suggerimenti, note particolari. Le domande a SCELTA_SINGOLA e SCELTA_MULTIPLA richiedono opzioni predefinite tramite l'entità OpzioneDomanda. Il sistema valida automaticamente che solo i tipi che richiedono opzioni possano averne.

DomandaQuestionario con Opzioni
@Entity
public class DomandaQuestionario {
    private Long id;
    private Questionario questionario;
    private String testo;
    private TipoDomanda tipoDomanda;
    private Boolean obbligatoria;        // Default: true
    private Integer ordine;
    private String descrizioneAiuto;     // Testo di aiuto opzionale
    private List<OpzioneDomanda> opzioni;
}

// Validazione nel metodo aggiungiOpzione
public OpzioneDomanda aggiungiOpzione(String testo) {
    if (!this.tipoDomanda.richiedeOpzioni()) {
        throw new IllegalStateException(
            "Le domande di tipo " + this.tipoDomanda
            + " non supportano opzioni");
    }
    // ...
}

Compilazioni e Risposte

Quando un utente compila un questionario, il sistema crea una CompilazioneQuestionario che raggruppa tutte le risposte. Il questionario supporta sia compilazioni da utenti registrati che compilazioni pubbliche (anonime o con dati forniti volontariamente).

CompilazioneQuestionario
@Entity
public class CompilazioneQuestionario {
    private Long id;
    private Questionario questionario;
    private User utente;                // null se compilazione pubblica
    private String nomeCompilatore;
    private String emailCompilatore;
    private String fingerprint;         // Per tracciamento anonimo
    private List<RispostaQuestionario> risposte;
    private Instant compilatoIl;
}

// Due modalità' di compilazione
CompilazioneQuestionario.creaPerUtente(questionario, utente);
CompilazioneQuestionario.creaPubblica(
    questionario, nome, email, fingerprint);

Ogni risposta è modellata da RispostaQuestionario, che gestisce sia il testo libero (per domande aperte) sia le opzioni selezionate (serializzate come JSON array per domande a scelta).

Etichette e Organizzazione

I questionari possono essere categorizzati tramite etichette con nome e colore personalizzabile. Il sistema offre etichette predefinite (di sistema) e la possibilità per ogni utente di crearne di personalizzate.

EtichettaQuestionario
@Entity
public class EtichettaQuestionario {
    private Long id;
    private String nome;          // Max 50 caratteri
    private String colore;        // Codice esadecimale (#3B82F6)
    private Long userId;          // null per etichette di sistema
    private boolean predefinita;  // true = di sistema
}

// Creazione etichetta personalizzata
EtichettaQuestionario.creaPersonalizzata(
    userId, "Feedback Post-Evento", "#10B981");

Risultati in Tempo Reale

La visibilità dei risultati è controllata da regole precise sia per i sondaggi che per i questionari. Per i sondaggi, il metodo sonoRisultatiVisibili() implementa la logica:

  • Se il sondaggio è CHIUSO: risultati sempre visibili a tutti
  • Se mostraRisultatiPrimaVoto è attivo: risultati visibili anche senza votare
  • Altrimenti: risultati visibili solo dopo aver votato
Logica Visibilità Risultati
public boolean sonoRisultatiVisibili(boolean haVotato) {
    if (this.stato == StatoSondaggio.CHIUSO) return true;
    if (Boolean.TRUE.equals(this.mostraRisultatiPrimaVoto)) return true;
    return haVotato;
}

public boolean isVotabile() {
    return this.stato == StatoSondaggio.ATTIVO &&
        (this.dataScadenza == null ||
         LocalDateTime.now().isBefore(this.dataScadenza));
}

I risultati vengono calcolati aggregando le risposte per ogni opzione. Ogni OpzioneSondaggio espone il conteggio tramite getNumeroVoti() che conta le risposte nella collection. Sul frontend, questi dati alimentano grafici a barre e percentuali aggiornati in tempo reale.

Punti Chiave

  • Macchina a stati con 4 transizioni controllate e possibilità di riapertura
  • Risposte SINGOLA e MULTIPLA con limite configurabile di opzioni selezionabili
  • Votazione anonima con tracciamento via fingerprint per anti-duplicazione
  • Sondaggi pubblici con UUID sharing codes per votanti esterni
  • Template riutilizzabili con deep copy completa di opzioni
  • Audit trail obbligatorio con motivazione per ogni cambio di vincitore
  • 5 tipi di entità sondaggio (LUOGO, DATA, MENU, ATTIVITA, CUSTOM)
  • Questionari con domande tipizzate (APERTA, SCELTA_SINGOLA, SCELTA_MULTIPLA)
  • Etichette personalizzabili con colori per organizzare i questionari
  • Visibilità risultati controllata con 3 livelli di accesso

Il codice sorgente del progetto è disponibile su GitHub. Per provare il sistema completo di sondaggi e questionari, visita www.playtheevent.com.

Domande frequenti

Come funziona la macchina a stati dei sondaggi in Play The Event?

La macchina a stati dei sondaggi segue una sequenza ben definita con quattro stati possibili (BOZZA, ATTIVO, CHIUSO e ARCHIVIATO) e transizioni controllate, garantendo che tutte le invarianti di business siano rispettate in ogni momento.

Qual è la differenza tra i tipi di risposta SINGOLA e MULTIPLA nei sondaggi?

Il tipo di risposta SINGOLA consente di selezionare un'unica opzione, mentre il tipo di risposta MULTIPLA consente di selezionare più opzioni (con un valore predefinito di 3, ma configurabile dall'organizzatore).

Come funziona la votazione anonima nei sondaggi?

La votazione anonima è attivata tramite un flag booleano e consente di registrare le risposte senza associarle all'utente votante. Il sistema utilizza il factory method per gestire le risposte anonime e prevenire voti duplicati.

Qual è la differenza tra i sondaggi di tipo EVENTO e TEMPLATE?

I sondaggi di tipo EVENTO sono collegati a un evento specifico, mentre i sondaggi di tipo TEMPLATE sono riutilizzabili come modello per creare istanze identiche in contesti diversi.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti