Documentazione: DD-051 e DD-052, modulo MVP, database e changelog

Voto MVP definitivo, a pari merito, segreto e a tempo; nome del votato dalla rosa. PROJECT_STATE
segna che m35-m38 restano da applicare su Supabase (db push rinviato) e il lavoro su feature/gara.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-26 09:59:40 +01:00
1 parent 5fed105850
commit 9d047a6ecd
7 files changed
+194 -53

No files matched your search

+38 -19
View File
@@ -2,6 +2,7 @@
**Stato:** implementato
**File principali:** `src/lib/mvp-voti.ts`, `src/components/crapp/VotazioneMvp.tsx`
**Test:** `test/unit/mvp-voti.test.ts`, `test/integration/permessi.test.ts` (regole del voto), `mvp-badge`, `s-tiebreak-badge`
---
@@ -15,7 +16,10 @@ calcolato a runtime.
## Dati
Tabella `mvp_voti`, vincolo `UNIQUE (match_id, votante_id)` — un solo voto per giocatore per
partita, sovrascrivibile.
partita, **definitivo** (DD-051) e **segreto** (DD-052): la tabella la legge solo chi ha votato (il
proprio voto) e gli admin; la squadra legge i conteggi dalla vista `mvp_voti_anonimi` (`match_id`,
`votato_id`, `votato_nome`, senza `votante_id`). `votato_nome` lo riempie un trigger dalla rosa a
partire da `votato_id`: il nome inviato dal client è ignorato.
`match_id` è l'**id dell'evento CrAPP**, non quello del referto CSI né dello Scout: la
votazione non dipende più da nessuna delle due fonti (i voti scritti prima con l'id scout/CSI
@@ -27,44 +31,59 @@ restano nel database ma non vengono più letti da nessuna schermata).
- Il pannello sta in `partita.$id.tsx` in una sezione sua, sempre presente: `votoMvpAperto()`
lo apre `ORE_ATTESA_MVP` (2) ore dopo `data`+`ora` dell'evento, prima di allora mostra solo
quando aprirà. Nessun legame con il risultato caricato.
quando aprirà; `votoMvpChiuso()` lo chiude `DURATA_VOTO_MVP_ORE` (24) ore dopo l'apertura
(DD-052). A votazione chiusa il pannello mostra «Votazione chiusa» e i risultati, senza il
pulsante «Vota». Nessun legame con il risultato caricato.
- Votano e sono votabili solo i **presenti** di quell'evento (`presente` o `ritardo` in
`usePresenzeEvento`): chi non c'era ha il bottone disabilitato e non compare nell'elenco.
- `useVotaMvp()` fa upsert `onConflict: match_id, votante_id`: il voto è modificabile senza
limiti, senza storico.
- `useVotiMvp()` legge i conteggi dalla vista `mvp_voti_anonimi`; `useMieiVotiMvp()` legge dalla
tabella solo i voti dell'utente (gli unici che la policy gli lascia), per «Hai votato X».
- `useVotaMvp()` fa un `INSERT`: il voto è definitivo. Il pannello chiede di scegliere un nome e di
confermarlo, poi mostra «Hai votato X: il voto non si può cambiare». A database (`m35`, DD-051) il
giocatore ha solo la policy di `INSERT`: modificare o cancellare un voto è possibile solo agli admin.
- Nessuno vota sé stesso: `VotazioneMvp.tsx` toglie il votante dall'elenco e il vincolo
`mvp_no_autovoto` (migration `m12_niente_autovoto`) rifiuta la riga anche a chi scrive
direttamente su PostgREST, come già faceva `pagelle_no_autovoto` per le pagelle.
- `conteggioPartita()`/`vincitoriMvp()` richiedono un margine netto: in caso di parità,
nessun vincitore viene assegnato per quella partita finché non arrivano altri voti.
- `vincitoriMvp()`/`mvpVintiPerGiocatore()` richiedono anche un quorum minimo di voti totali
- `mvpPartita()` restituisce i più votati: **in caso di parità in testa sono tutti MVP a pari merito**
(DD-051). `vincitoriMvp()` ne unisce i nomi («Anna e Bruno») per Home e Classifica.
- `mvpPartita()` (quindi anche `vincitoriMvp()` e `mvpVintiPerGiocatore()`) richiede un quorum minimo di voti totali
sulla partita (`VOTI_MINIMI_MVP = 2`, `mvp-voti.ts`, DD-028): un solo voto non basta a
incoronare nessuno, nemmeno senza concorrenza.
- `mvpVintiPerGiocatore()` conta una vittoria per ogni partita "vinta" con margine netto; il
risultato alimenta il campo `mvp` del `Giocatore` in `useRosa()`, mostrato come StatTile
- `mvpVintiPerGiocatore()` conta una vittoria per ogni partita in cui il giocatore è tra i più votati
(anche a pari merito); il risultato alimenta il campo `mvp` del `Giocatore` in `useRosa()`, mostrato come StatTile
nel profilo e in home.
---
## Limiti noti
- Nessuna scadenza o chiusura della votazione: una volta aperta resta aperta indefinitamente.
- Il voto si chiude 24 ore dopo l'apertura (DD-052). Le partite più vecchie sono quindi chiuse:
chi non aveva votato non può più farlo. Gli admin possono ancora scrivere un voto dal database.
- Gli admin leggono la tabella `mvp_voti` (gli serve per correggere un voto), quindi a livello di
database vedono chi ha votato chi; l'app non lo mostra a nessuno.
- I conteggi sono visibili anche durante la votazione. Nascondere i risultati fino alla chiusura è una
possibile evoluzione.
- Il voto è legato a chi lo scrive: da `m11_scritture_per_ruolo` la policy impone che
`votante_id` sia lo slot collegato all'account (DD-023). Su chi viene votato l'unico
vincolo diretto è che non sia il votante stesso (`mvp_no_autovoto`).
Da `m13_convocati_e_pagelle_chiuse` la stessa policy verifica anche che **sia il votante sia
il votato** siano tra i **convocati** dell'evento (`evento_permette_voto()`, convocati vuoto
= tutta la rosa): prima era un filtro solo applicativo, ora un giocatore non convocato non
può più votare né essere votato scrivendo direttamente su PostgREST. Restano invece solo
applicativi, non controllati da nessuna policy: che votante e votato fossero **presenti**
(non solo convocati: `presente`/`ritardo` in `usePresenzeEvento`, un controllo più stretto
della sola convocazione) a quella partita, e le due ore d'attesa dall'inizio evento
(`votoMvpAperto()`) — un amministratore, o chiunque scriva su PostgREST, passa comunque.
- In caso di parità, o sotto il quorum minimo di voti, nessun MVP viene assegnato per quella
partita.
= tutta la rosa). Da `m36` (DD-051) la policy usa `voto_mvp_consentito()`, che aggiunge a quei
controlli: l'evento è una **partita**, votante e votato hanno risposto `presente` o `ritardo` e
sono passate **2 ore** dall'inizio e **non ancora altre 24** (`Europe/Rome`; un `ora` vuoto
vale mezzanotte). Sono le stesse
regole del pannello, ora imposte anche a chi scrive direttamente su PostgREST; l'attesa di 2 ore
e la chiusura stanno in `ORE_ATTESA_MVP`/`DURATA_VOTO_MVP_ORE` e negli intervalli della funzione
(`m37`); un test unitario li tiene allineati. Gli
admin restano fuori da questi vincoli (possono correggere un voto). La presenza si controlla solo
al momento del voto: se qualcuno cambia risposta dopo, il voto già dato resta.
- Sotto il quorum minimo di voti nessun MVP viene assegnato per quella partita. Una parità in testa,
invece, assegna l'MVP a tutti i pari merito.
- Un voto sbagliato si corregge solo da un admin (dal database): non c'è un'interfaccia per farlo.
---
## Evoluzioni possibili
- Introdurre una scadenza (es. la votazione si chiude N giorni dopo la partita).
- Nascondere i risultati finché la votazione non è chiusa.
- Una schermata per gli admin per correggere un voto (oggi solo dal database).