From ff02b7439c7b8383e6d9a1b42115679631b65781 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Tue, 6 Oct 2026 10:38:19 +0200 Subject: [PATCH] Obiettivo evento di squadra al mese: conta quando arriva l'ora dell'evento MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prima bastava definire l'evento nel mese; ora data e ora (Europe/Rome) devono essere già passate. Senza ora vale 00:00. Test e documentazione aggiornati. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 5 +++ docs/modules/obiettivi-squadra.md | 8 ++++- src/lib/obiettivi.ts | 26 ++++++++++++++- test/unit/obiettivi.test.ts | 55 ++++++++++++++++++++++++++++++- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a1e0b57..dccbd8b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,11 @@ versioni `0.y.z` erano pre-release. messaggio a testa. Push, centro notifiche ed email. Il 29 febbraio non è gestito (DD-047, issue #11). Migration `m31_notifica_compleanno`, da applicare in produzione. +### Modificato + +- **Obiettivo «1 evento di squadra al mese»** — si completa quando arriva l'ora dell'evento, non appena viene + creato. Un evento futuro non conta finché la sua data e ora non sono passate. Nessuna migration. + ## [1.2.3] - 2026-10-04 Nella Home si vede anche chi non ha caricato il certificato medico, e le dipendenze sono aggiornate perché Vercel bloccava il deploy. Nessuna migration. diff --git a/docs/modules/obiettivi-squadra.md b/docs/modules/obiettivi-squadra.md index 248f3bd..2c8ee75 100644 --- a/docs/modules/obiettivi-squadra.md +++ b/docs/modules/obiettivi-squadra.md @@ -48,7 +48,7 @@ corrente dinamico (vedi sotto). | `o3` | Prima vittoria del campionato | `min(vittorie, 1)` | 1 | JSON partite CSI (vedi sotto) | | `o4` | 5 vittorie in campionato | `min(vittorie, 5)` | 5 | JSON partite CSI | | `o5` | 10 vittorie in campionato | `min(vittorie, 10)` | 10 | JSON partite CSI | -| `o6` | 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente (dinamico) | 1 | `eventi_app` | +| `o6` | 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente la cui data e ora sono già passate (dinamico) | 1 | `eventi_app` | Mostrati in `squadra.tsx` (elenco completo con barra di progresso) e in `index.tsx` (home: il primo obiettivo non completato). Un obiettivo che supera il 90% genera anche una notifica @@ -67,6 +67,12 @@ automaticamente a ogni cambio mese: il mese di riferimento è calcolato dalla da ("90% di presenze ad agosto" / "a settembre" / ...) e scadenza (ultimo giorno del mese) seguono di conseguenza. +`o6` si completa quando **arriva l'ora dell'evento**, non quando lo si definisce: conta solo +un evento di tipo "evento" del mese corrente il cui `data` + `ora` (fuso Europe/Rome, +`adessoRoma(oggi)`) è già passato; senza ora vale `00:00`. Un evento futuro lascia l'obiettivo a +0/1 finché non scatta l'ora. Il valore si ricalcola al render: con l'app già aperta il +passaggio a completato compare al refresh successivo. + `o2` ("Tutti rispondono alle convocazioni") non si azzera — aggrega su tutti gli eventi in programma, non solo quelli del mese corrente — ma la sua `scadenza` mostrata in interfaccia è anch'essa l'ultimo giorno del mese corrente (`fineMese(oggi)`), non più una data fissa. diff --git a/src/lib/obiettivi.ts b/src/lib/obiettivi.ts index 2ba8d47..a1260be 100644 --- a/src/lib/obiettivi.ts +++ b/src/lib/obiettivi.ts @@ -31,6 +31,24 @@ function meseCorrente(oggi: Date): string { return new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(oggi).slice(0, 7); } +/** Istante corrente in Europe/Rome come "YYYY-MM-DD HH:mm", confrontabile con data + ora di un evento. */ +function adessoRoma(oggi: Date): string { + const p = Object.fromEntries( + new Intl.DateTimeFormat("en-CA", { + timeZone: "Europe/Rome", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hourCycle: "h23", + }) + .formatToParts(oggi) + .map((x) => [x.type, x.value]), + ); + return `${p["year"]}-${p["month"]}-${p["day"]} ${p["hour"]}:${p["minute"]}`; +} + /** "a settembre" / "ad agosto": preposizione con elisione davanti a vocale. */ function aMese(oggi: Date): string { const nome = new Intl.DateTimeFormat("it-IT", { timeZone: "Europe/Rome", month: "long" }).format( @@ -180,7 +198,13 @@ export function obiettiviSquadra( id: "o6", titolo: "1 evento di squadra al mese", descrizione: "Pizzate, cene e uscite fuori dal campo", - valore: ctx.eventi.filter((e) => e.tipo === "evento" && e.data.startsWith(mese)).length, + // Conta solo quando l'ora dell'evento è arrivata, non quando viene definito. + valore: ctx.eventi.filter( + (e) => + e.tipo === "evento" && + e.data.startsWith(mese) && + `${e.data} ${e.ora || "00:00"}` <= adessoRoma(oggi), + ).length, target: 1, unita: "eventi", emoji: "🍕", diff --git a/test/unit/obiettivi.test.ts b/test/unit/obiettivi.test.ts index 24fad30..85d3634 100644 --- a/test/unit/obiettivi.test.ts +++ b/test/unit/obiettivi.test.ts @@ -216,7 +216,7 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2 // --- o6: evento di squadra al mese, si azzera come o1 ------------------------ { - const OGGI_SETTEMBRE = new Date("2026-09-05T10:00:00Z"); + const OGGI_SETTEMBRE = new Date("2026-09-20T10:00:00Z"); const pizzataSettembre: ContestoObiettivi = { eventi: [evento("p1", "2026-09-12", "evento")], presenze: {}, @@ -233,6 +233,59 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2 "lo stesso evento non conta se oggi è agosto", ); + // Definito ma non ancora arrivato: non conta finché non scatta l'ora dell'evento. + const pizzataFutura: ContestoObiettivi = { + eventi: [{ ...evento("pf1", "2026-09-28", "evento"), ora: "20:30" }], + presenze: {}, + pagelle: [], + }; + assert.equal( + trova(obiettiviSquadra(giocatori, pizzataFutura, OGGI_SETTEMBRE), "o6").valore, + 0, + "un evento futuro non completa l'obiettivo al momento della definizione", + ); + const serata = { ...evento("ps1", "2026-09-20", "evento"), ora: "20:30" }; + const conSerata: ContestoObiettivi = { eventi: [serata], presenze: {}, pagelle: [] }; + // 20/09 10:00Z = 12:00 a Roma (CEST): prima delle 20:30 non conta, dopo sì. + assert.equal( + trova(obiettiviSquadra(giocatori, conSerata, OGGI_SETTEMBRE), "o6").valore, + 0, + "stesso giorno ma ora non ancora arrivata: non conta", + ); + assert.equal( + trova(obiettiviSquadra(giocatori, conSerata, new Date("2026-09-20T18:30:00Z")), "o6").valore, + 1, + "arrivata l'ora dell'evento (20:30 a Roma): conta", + ); + + // Senza ora vale 00:00: conta dal giorno stesso, non il giorno prima. + const senzaOra: ContestoObiettivi = { + eventi: [{ ...evento("so1", "2026-09-20", "evento"), ora: "" }], + presenze: {}, + pagelle: [], + }; + assert.equal( + trova(obiettiviSquadra(giocatori, senzaOra, OGGI_SETTEMBRE), "o6").valore, + 1, + "evento senza ora: conta dal giorno stesso", + ); + assert.equal( + trova(obiettiviSquadra(giocatori, senzaOra, new Date("2026-09-19T21:00:00Z")), "o6").valore, + 0, + "evento senza ora: la sera prima (Roma) non conta", + ); + // Più eventi: ne basta uno passato; quello futuro non si somma. + const misti: ContestoObiettivi = { + eventi: [evento("m1", "2026-09-05", "evento"), evento("m2", "2026-09-28", "evento")], + presenze: {}, + pagelle: [], + }; + assert.equal( + trova(obiettiviSquadra(giocatori, misti, OGGI_SETTEMBRE), "o6").valore, + 1, + "conta solo l'evento già passato", + ); + const allenamentoNelMese: ContestoObiettivi = { eventi: [evento("al1", "2026-08-12", "allenamento")], presenze: {},