diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 16be79f..e772b8a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -38,9 +38,9 @@ Prima versione, pre-release. della partita, e può usarlo chiunque sia autenticato: uno per volta, grazie al lock. - Votazione MVP legata all'evento CrAPP e non al referto CSI o allo Scout: si apre due ore dopo `data`+`ora` della partita, anche senza risultato caricato — [modules/mvp.md](modules/mvp.md). -- Sondaggio pre-partita con apertura programmata alle 8:00 del giorno della partita e - pulsante «Avvisa tutti del sondaggio» per gli amministratori - (`POST /api/public/apri-sondaggio`); nessun cron, l'invio è manuale. +- Sondaggio pre-partita aperto dalle 8:00 del giorno della partita fino al fischio d'inizio + (poi resta chiuso, anche nei giorni successivi) e pulsante «Avvisa tutti del sondaggio» per + gli amministratori (`POST /api/public/apri-sondaggio`); nessun cron, l'invio è manuale. - Turni palloni con rotazione automatica sulle partite e assegnazione manuale per gli allenamenti, che restano «da assegnare» finché non si sceglie (migration M10). - Notifiche push con il testo cifrato **dentro** la push (`aes128gcm`, RFC 8291), così diff --git a/src/components/crapp/SondaggioCacche.tsx b/src/components/crapp/SondaggioCacche.tsx index b617814..96689e3 100644 --- a/src/components/crapp/SondaggioCacche.tsx +++ b/src/components/crapp/SondaggioCacche.tsx @@ -7,7 +7,13 @@ import { nomeCompleto, useGiocatoriSquadra } from "@/lib/giocatori-squadra"; import { useGiocatoreCorrente } from "@/lib/user-store"; import { intestazioniAutenticate } from "@/lib/auth"; import { useIsAdmin } from "@/lib/ruoli"; -import { mediaPartita, sondaggioAperto, useCacche, useSalvaCacche } from "@/lib/cacche"; +import { + mediaPartita, + sondaggioAperto, + sondaggioTerminato, + useCacche, + useSalvaCacche, +} from "@/lib/cacche"; const opzioni = [0, 1, 2, 3, 4, 5]; @@ -15,9 +21,11 @@ const opzioni = [0, 1, 2, 3, 4, 5]; export function SondaggioCacche({ eventoId, dataEvento, + oraEvento, }: { eventoId: string; dataEvento: string; + oraEvento: string; }) { const io = useGiocatoreCorrente(); const { righe } = useCacche(); @@ -69,14 +77,16 @@ export function SondaggioCacche({ } } - if (!sondaggioAperto(dataEvento)) { + if (!sondaggioAperto(dataEvento, oraEvento)) { return (

💩 Sondaggio pre-partita

- Apre alle 8:00 del giorno della partita: riceverai una notifica. + {sondaggioTerminato(dataEvento, oraEvento) + ? "Sondaggio chiuso: era aperto fino al fischio d'inizio." + : "Apre alle 8:00 del giorno della partita: riceverai una notifica."}

); diff --git a/src/lib/cacche.ts b/src/lib/cacche.ts index 09ef080..42577d3 100644 --- a/src/lib/cacche.ts +++ b/src/lib/cacche.ts @@ -5,11 +5,40 @@ import { oggiISO } from "./palloni-core"; /** Ora di apertura del sondaggio, il giorno stesso della partita. */ export const ORA_APERTURA_SONDAGGIO = 8; -/** Il sondaggio apre alle 8:00 del giorno della partita e da lì resta aperto. */ -export function sondaggioAperto(dataEvento: string, adesso = new Date()): boolean { +/** Istante del fischio d'inizio: `oraEvento` mancante equivale a fine giornata (non chiude mai prima). */ +function inizioPartita(dataEvento: string, oraEvento: string): number { + return new Date(`${dataEvento}T${oraEvento || "23:59"}:00`).getTime(); +} + +/** Il sondaggio apre alle 8:00 del giorno della partita e chiude al fischio d'inizio. */ +export function sondaggioAperto( + dataEvento: string, + oraEvento: string, + adesso = new Date(), +): boolean { + if (dataEvento !== oggiISO(adesso)) return false; + const apertura = new Date(`${dataEvento}T00:00:00`); + apertura.setHours(ORA_APERTURA_SONDAGGIO, 0, 0, 0); + return ( + adesso.getTime() >= apertura.getTime() && + adesso.getTime() < inizioPartita(dataEvento, oraEvento) + ); +} + +/** + * True se il fischio d'inizio è già passato (oggi o in un giorno precedente): serve a + * distinguere, nel messaggio mostrato quando il sondaggio è chiuso, "non ancora aperto" + * da "già chiuso" — altrimenti dopo la partita si continuerebbe a dire "apre alle 8:00". + */ +export function sondaggioTerminato( + dataEvento: string, + oraEvento: string, + adesso = new Date(), +): boolean { const oggi = oggiISO(adesso); - if (dataEvento !== oggi) return dataEvento < oggi; - return adesso.getHours() >= ORA_APERTURA_SONDAGGIO; + if (dataEvento < oggi) return true; + if (dataEvento > oggi) return false; + return adesso.getTime() >= inizioPartita(dataEvento, oraEvento); } /** Sondaggio goliardico pre-partita: quante cacche prima del match di campionato. */ diff --git a/src/routes/partita.$id.tsx b/src/routes/partita.$id.tsx index c91805f..d162f4d 100644 --- a/src/routes/partita.$id.tsx +++ b/src/routes/partita.$id.tsx @@ -197,7 +197,7 @@ function PartitaDetail() {
- +
{match ? ( diff --git a/test/unit/cacche.test.ts b/test/unit/cacche.test.ts index 152d400..bf769aa 100644 --- a/test/unit/cacche.test.ts +++ b/test/unit/cacche.test.ts @@ -5,6 +5,7 @@ import { mediaStagione, recordStagione, sondaggioAperto, + sondaggioTerminato, statisticheCacche, type RigaCacche, } from "@/lib/cacche"; @@ -54,11 +55,67 @@ assert.deepEqual( ); // --- sondaggioAperto --------------------------------------------------------- -const alle = (h: number, m = 0) => new Date(2026, 8, 5, h, m); // 5 settembre 2026 -assert.equal(sondaggioAperto("2026-09-05", alle(7, 59)), false, "prima delle 8 è chiuso"); -assert.equal(sondaggioAperto("2026-09-05", alle(8)), true, "alle 8 in punto apre"); -assert.equal(sondaggioAperto("2026-09-05", alle(23)), true); -assert.equal(sondaggioAperto("2026-09-06", alle(23)), false, "partita di domani: chiuso"); -assert.equal(sondaggioAperto("2026-09-04", alle(0, 1)), true, "partita passata: resta aperto"); +// Partita del 5 settembre 2026 alle 21:00. +const alle = (h: number, m = 0) => new Date(2026, 8, 5, h, m); +const ORA_PARTITA = "21:00"; + +assert.equal( + sondaggioAperto("2026-09-05", ORA_PARTITA, alle(7, 59)), + false, + "prima delle 8 è chiuso", +); +assert.equal(sondaggioAperto("2026-09-05", ORA_PARTITA, alle(8)), true, "alle 8 in punto apre"); +assert.equal( + sondaggioAperto("2026-09-05", ORA_PARTITA, alle(20, 59)), + true, + "un minuto prima del fischio d'inizio è ancora aperto", +); +assert.equal( + sondaggioAperto("2026-09-05", ORA_PARTITA, alle(21, 0)), + false, + "al fischio d'inizio si chiude", +); +assert.equal( + sondaggioAperto("2026-09-05", ORA_PARTITA, alle(23)), + false, + "dopo l'inizio resta chiuso per il resto della giornata", +); +assert.equal( + sondaggioAperto("2026-09-06", ORA_PARTITA, alle(23)), + false, + "partita di domani: chiuso", +); +assert.equal( + sondaggioAperto("2026-09-04", ORA_PARTITA, alle(0, 1)), + false, + "partita passata: chiuso anche nei giorni successivi", +); + +// --- sondaggioTerminato: distingue "non ancora aperto" da "già chiuso" ------- +assert.equal( + sondaggioTerminato("2026-09-05", ORA_PARTITA, alle(7, 59)), + false, + "prima dell'apertura: non ancora aperto, non terminato", +); +assert.equal( + sondaggioTerminato("2026-09-05", ORA_PARTITA, alle(20, 59)), + false, + "in corso: non ancora terminato", +); +assert.equal( + sondaggioTerminato("2026-09-05", ORA_PARTITA, alle(21, 0)), + true, + "al fischio d'inizio: terminato", +); +assert.equal( + sondaggioTerminato("2026-09-06", ORA_PARTITA, alle(23)), + false, + "partita di domani: non ancora, non terminato", +); +assert.equal( + sondaggioTerminato("2026-09-04", ORA_PARTITA, alle(0, 1)), + true, + "partita passata: terminato", +); console.log("cacche: ok");