Obiettivo evento di squadra al mese: conta quando arriva l'ora dell'evento

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 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-06 10:38:19 +02:00
1 parent 985fc2fcc1
commit ff02b7439c
4 files changed
+91 -3

No files matched your search

+5
View File
@@ -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.
+7 -1
View File
@@ -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.
+25 -1
View File
@@ -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: "🍕",
+54 -1
View File
@@ -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: {},