Apre il voto MVP due ore dopo l'inizio e lo riserva ai presenti.
La votazione non dipendeva dal tempo ma dal risultato: compariva solo con un referto CSI o uno scout salvato, e chiunque avesse un profilo poteva votare. Ora `votoMvpAperto()` la apre due ore dopo `data`+`ora` dell'evento, il pannello sta in una sezione sua e votano — e sono votabili — solo i presenti (o in ritardo) di quell'evento. I voti passano dall'id scout/CSI a quello dell'evento CrAPP: home e classifica leggono il vincitore tramite la mappa data → evento che avevano già. Le regole sono applicative, non RLS: chi scrive su PostgREST le aggira, come già annotato tra i limiti noti del modulo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,15 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
## Versione attuale — agosto 2026
|
||||
|
||||
### Il voto MVP apre due ore dopo il fischio d'inizio, e vota solo chi c'era
|
||||
|
||||
- La votazione non dipende più dal referto CSI né dallo Scout: il pannello ha una sezione sua
|
||||
in `/partita/$id` e si apre due ore dopo `data`+`ora` dell'evento, anche senza risultato
|
||||
caricato.
|
||||
- Votano — e sono votabili — solo i giocatori dati `presente` o `ritardo` a quell'evento.
|
||||
- I voti sono ora legati all'id dell'evento CrAPP: le righe scritte prima con l'id dello
|
||||
scout o della partita CSI restano nel database ma non vengono più lette.
|
||||
|
||||
### Tre riletture in meno dopo ogni salvataggio
|
||||
|
||||
- «Aggiungi giocatore», salvataggio ed eliminazione di una partita scoutata e cambio della
|
||||
|
||||
+13
-5
@@ -17,12 +17,19 @@ calcolato a runtime.
|
||||
Tabella `mvp_voti`, vincolo `UNIQUE (match_id, votante_id)` — un solo voto per giocatore per
|
||||
partita, sovrascrivibile.
|
||||
|
||||
`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
|
||||
restano nel database ma non vengono più letti da nessuna schermata).
|
||||
|
||||
---
|
||||
|
||||
## Implementazione
|
||||
|
||||
- Il pannello compare in `partita.$id.tsx` solo se esiste un risultato (Scout Live salvato)
|
||||
per la partita, altrimenti mostra "la partita non è ancora stata disputata".
|
||||
- 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.
|
||||
- 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.
|
||||
- Nessuno vota sé stesso: `VotazioneMvp.tsx` toglie il votante dall'elenco e il vincolo
|
||||
@@ -38,11 +45,12 @@ partita, sovrascrivibile.
|
||||
|
||||
## Limiti noti
|
||||
|
||||
- Nessuna scadenza o chiusura della votazione: resta aperta indefinitamente.
|
||||
- Nessuna scadenza o chiusura della votazione: una volta aperta resta aperta indefinitamente.
|
||||
- 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 è che non sia il votante stesso (`mvp_no_autovoto`): che sia un convocato di
|
||||
quella partita resta un filtro solo applicativo.
|
||||
vincolo è che non sia il votante stesso (`mvp_no_autovoto`): che votante e votato fossero
|
||||
presenti a quella partita, e che siano passate due ore dall'inizio, restano filtri solo
|
||||
applicativi — chi scrive su PostgREST li aggira.
|
||||
- In caso di parità, nessun MVP viene assegnato per quella partita.
|
||||
|
||||
---
|
||||
|
||||
@@ -4,15 +4,34 @@ import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { nomeCompleto, useGiocatoriSquadra } from "@/lib/giocatori-squadra";
|
||||
import { useGiocatoreCorrente } from "@/lib/user-store";
|
||||
import { conteggioPartita, mioVoto, useVotaMvp, useVotiMvp, type VotoMvp } from "@/lib/mvp-voti";
|
||||
import { usePresenzeEvento } from "@/lib/presenze";
|
||||
import type { Evento } from "@/lib/eventi";
|
||||
import {
|
||||
conteggioPartita,
|
||||
mioVoto,
|
||||
ORE_ATTESA_MVP,
|
||||
useVotaMvp,
|
||||
useVotiMvp,
|
||||
votoMvpAperto,
|
||||
type VotoMvp,
|
||||
} from "@/lib/mvp-voti";
|
||||
|
||||
/** Pannello di votazione MVP di una partita: un voto a testa, modificabile. */
|
||||
export function VotazioneMvp({ matchId }: { matchId: string }) {
|
||||
/**
|
||||
* Pannello di votazione MVP di una partita: un voto a testa, modificabile.
|
||||
*
|
||||
* I voti sono legati all'evento CrAPP, non al referto CSI né allo scout: si vota anche
|
||||
* senza risultato caricato, dalle due ore dopo il fischio d'inizio. Votano e sono votabili
|
||||
* solo i presenti (o in ritardo) di quell'evento.
|
||||
*/
|
||||
export function VotazioneMvp({ evento }: { evento: Evento }) {
|
||||
const matchId = evento.id;
|
||||
const io = useGiocatoreCorrente();
|
||||
const voti = useVotiMvp();
|
||||
const vota = useVotaMvp();
|
||||
const { righe: squadra } = useGiocatoriSquadra();
|
||||
const rosa = squadra.filter((g) => g.attivo);
|
||||
const { risposte } = usePresenzeEvento(evento.id);
|
||||
const presente = (id: string) => risposte[id] === "presente" || risposte[id] === "ritardo";
|
||||
const rosa = squadra.filter((g) => g.attivo && presente(g.id));
|
||||
const [aperto, setAperto] = useState(false);
|
||||
|
||||
const tutti: VotoMvp[] = voti.data ?? [];
|
||||
@@ -21,6 +40,20 @@ export function VotazioneMvp({ matchId }: { matchId: string }) {
|
||||
const totale = conteggio.reduce((s, c) => s + c.voti, 0);
|
||||
const testa = conteggio[0];
|
||||
const pareggio = conteggio.length > 1 && conteggio[1]!.voti === testa?.voti;
|
||||
const puoVotare = !!io && presente(io.id);
|
||||
|
||||
if (!votoMvpAperto(evento.data, evento.ora)) {
|
||||
return (
|
||||
<div className="mt-3 rounded-2xl bg-secondary/60 p-3">
|
||||
<p className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<Vote className="h-3.5 w-3.5" /> Voto MVP
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Apre {ORE_ATTESA_MVP} ore dopo l'inizio della partita.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function invia(id: string, nome: string) {
|
||||
if (!io) return;
|
||||
@@ -47,7 +80,7 @@ export function VotazioneMvp({ matchId }: { matchId: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAperto((v) => !v)}
|
||||
disabled={!io}
|
||||
disabled={!puoVotare}
|
||||
className="rounded-full bg-accent px-3 py-1 text-xs font-bold uppercase text-accent-foreground disabled:opacity-50"
|
||||
>
|
||||
{mio ? "Cambia voto" : "Vota"}
|
||||
@@ -67,6 +100,10 @@ export function VotazioneMvp({ matchId }: { matchId: string }) {
|
||||
</p>
|
||||
{mio ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Hai votato {mio.votato_nome}</p>
|
||||
) : !puoVotare ? (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Vota chi era presente a questa partita.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{aperto ? (
|
||||
|
||||
@@ -10,6 +10,16 @@ export type VotoMvp = {
|
||||
|
||||
const CHIAVE = ["mvp-voti"] as const;
|
||||
|
||||
/** Ore da aspettare dall'inizio della partita prima di poter votare l'MVP. */
|
||||
export const ORE_ATTESA_MVP = 2;
|
||||
|
||||
/** Il voto MVP apre 2 ore dopo l'inizio della partita e da lì resta aperto. */
|
||||
export function votoMvpAperto(data: string, ora: string, adesso = new Date()): boolean {
|
||||
const inizio = new Date(`${data}T${ora || "00:00"}:00`);
|
||||
if (Number.isNaN(inizio.getTime())) return false;
|
||||
return adesso.getTime() >= inizio.getTime() + ORE_ATTESA_MVP * 60 * 60_000;
|
||||
}
|
||||
|
||||
/** Tutti i voti MVP della squadra (poche righe, si carica tutto).
|
||||
* Nessun polling: la lista si aggiorna dopo il proprio voto o al rientro sull'app. */
|
||||
export function useVotiMvp() {
|
||||
|
||||
@@ -56,12 +56,13 @@ function Classifica() {
|
||||
const eventoIdPerData = new Map(
|
||||
eventi.filter((e) => e.tipo === "partita").map((e) => [e.data, e.id]),
|
||||
);
|
||||
// I voti MVP sono legati all'evento CrAPP, non al referto CSI né allo scout.
|
||||
const mvpPerData = (data: string) => mvpPerMatch[eventoIdPerData.get(data) ?? ""] ?? "";
|
||||
const tuttiMatch = csiGiocate.length
|
||||
? csiGiocate.map((p) => ({
|
||||
...matchDaPartitaCsi(p),
|
||||
mvp: mvpPerMatch[p.id] ?? "",
|
||||
scout: false,
|
||||
}))
|
||||
? csiGiocate.map((p) => {
|
||||
const m = matchDaPartitaCsi(p);
|
||||
return { ...m, mvp: mvpPerData(m.data), scout: false };
|
||||
})
|
||||
: scoutMatches.map((m) => ({
|
||||
id: m.id,
|
||||
data: m.data,
|
||||
@@ -70,7 +71,7 @@ function Classifica() {
|
||||
setNostri: m.setNostri,
|
||||
setLoro: m.setLoro,
|
||||
parziali: m.parziali,
|
||||
mvp: mvpPerMatch[m.id] ?? "",
|
||||
mvp: mvpPerData(m.data),
|
||||
scout: true,
|
||||
}));
|
||||
|
||||
|
||||
@@ -49,15 +49,15 @@ function Index() {
|
||||
const votiMvp = useVotiMvp();
|
||||
const mvpPerMatch = vincitoriMvp(votiMvp.data ?? []);
|
||||
const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
|
||||
const ultima = csiGiocate[0]
|
||||
? { ...matchDaPartitaCsi(csiGiocate[0]), mvp: mvpPerMatch[csiGiocate[0].id] ?? "" }
|
||||
: scoutMatches[0]
|
||||
? { ...scoutMatches[0], mvp: mvpPerMatch[scoutMatches[0].id] ?? "" }
|
||||
: null;
|
||||
const ultimaBase = csiGiocate[0] ? matchDaPartitaCsi(csiGiocate[0]) : (scoutMatches[0] ?? null);
|
||||
const obiettivi = useObiettivi();
|
||||
const obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null;
|
||||
const eventoUltima = ultima
|
||||
? (eventi.find((e) => e.tipo === "partita" && e.data === ultima.data) ?? null)
|
||||
const eventoUltima = ultimaBase
|
||||
? (eventi.find((e) => e.tipo === "partita" && e.data === ultimaBase.data) ?? null)
|
||||
: null;
|
||||
// I voti MVP sono legati all'evento CrAPP, non al referto CSI né allo scout.
|
||||
const ultima = ultimaBase
|
||||
? { ...ultimaBase, mvp: (eventoUltima && mvpPerMatch[eventoUltima.id]) ?? "" }
|
||||
: null;
|
||||
|
||||
if (!giocatore) return null;
|
||||
|
||||
@@ -169,14 +169,16 @@ function PartitaDetail() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-center text-xs font-semibold text-muted-foreground">
|
||||
MVP eletto dalla squadra
|
||||
</p>
|
||||
<VotazioneMvp matchId={match.id} />
|
||||
</div>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
<Section titolo="MVP eletto dalla squadra">
|
||||
<div className="rounded-3xl bg-card p-5 shadow-card">
|
||||
<VotazioneMvp evento={evento} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{match ? (
|
||||
<Section titolo="Badge votati dai compagni">
|
||||
<VotoSocial matchId={match.id} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
mioVoto,
|
||||
mvpVintiPerGiocatore,
|
||||
vincitoriMvp,
|
||||
votoMvpAperto,
|
||||
type VotoMvp,
|
||||
} from "@/lib/mvp-voti";
|
||||
|
||||
@@ -74,4 +75,14 @@ assert.deepEqual(
|
||||
);
|
||||
assert.deepEqual(mvpVintiPerGiocatore([]), {});
|
||||
|
||||
// --- votoMvpAperto: due ore dopo il fischio d'inizio -------------------------
|
||||
const alle = (o: number, m = 0) => new Date(2026, 8, 5, o, m);
|
||||
assert.equal(votoMvpAperto("2026-09-05", "20:30", alle(22, 29)), false, "un minuto prima");
|
||||
assert.equal(votoMvpAperto("2026-09-05", "20:30", alle(22, 30)), true, "due ore in punto");
|
||||
assert.equal(votoMvpAperto("2026-09-05", "20:30", alle(23)), true);
|
||||
assert.equal(votoMvpAperto("2026-09-06", "10:00", alle(23)), false, "partita di domani");
|
||||
assert.equal(votoMvpAperto("2026-09-04", "20:30", alle(0, 1)), true, "partita passata: aperto");
|
||||
assert.equal(votoMvpAperto("2026-09-05", "", alle(1, 59)), false, "senza ora vale mezzanotte");
|
||||
assert.equal(votoMvpAperto("2026-09-05", "", alle(2)), true);
|
||||
|
||||
console.log("mvp-voti: ok");
|
||||
|
||||
Reference in New Issue
Block a user