Rende dinamico il mese degli obiettivi di squadra invece di una costante fissa
"Presenze del mese" e "evento di squadra al mese" erano ancorati a una costante MESE = "2026-08": passato agosto restavano congelati sul mese scorso invece di azzerarsi a ogni cambio mese. Ora il mese di riferimento (e per il primo anche titolo e scadenza) segue la data corrente, con un parametro `oggi` iniettabile per test deterministici. Aggiunti unit test e un integration test end-to-end contro il database locale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Modulo — Obiettivi di squadra
|
# Modulo — Obiettivi di squadra
|
||||||
|
|
||||||
**Stato:** implementato (v1.0), con costanti stagionali da aggiornare a mano
|
**Stato:** implementato (v1.0), con target stagionali da aggiornare a mano (i mesi di
|
||||||
|
riferimento sono ora dinamici)
|
||||||
**File principali:** `src/lib/obiettivi.ts`, `src/lib/rosa.ts` (`useObiettivi()`)
|
**File principali:** `src/lib/obiettivi.ts`, `src/lib/rosa.ts` (`useObiettivi()`)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -25,14 +26,14 @@ serie).
|
|||||||
|
|
||||||
| Obiettivo | Calcolo | Target | Fonte |
|
| Obiettivo | Calcolo | Target | Fonte |
|
||||||
| ---------------------------------- | ----------------------------------------------- | ------ | ---------------------------------------------- |
|
| ---------------------------------- | ----------------------------------------------- | ------ | ---------------------------------------------- |
|
||||||
| 90% presenze ad agosto | risposte presente/ritardo sugli eventi del mese | 90% | `risposte_presenze` |
|
| 90% presenze del mese | risposte presente/ritardo sugli eventi del mese corrente (dinamico) | 90% | `risposte_presenze` |
|
||||||
| Tutti rispondono alle convocazioni | risposte totali / eventi possibili | 90% | `risposte_presenze` |
|
| Tutti rispondono alle convocazioni | risposte totali / eventi possibili | 90% | `risposte_presenze` |
|
||||||
| 250 presenze complessive | somma presenze di tutta la rosa | 250 | aggregato da `useRosa()` |
|
| 250 presenze complessive | somma presenze di tutta la rosa | 250 | aggregato da `useRosa()` |
|
||||||
| Media pagelle da 7.5 | media di squadra | 7.5 | `pagelle_voti` |
|
| Media pagelle da 7.5 | media di squadra | 7.5 | `pagelle_voti` |
|
||||||
| 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
| 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
||||||
| Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 | `serieAllenamenti` |
|
| Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 | `serieAllenamenti` |
|
||||||
| 1 / 5 / 10 vittorie in campionato | partite vinte da dati CSI ufficiali | 1/5/10 | modulo [Collegamento CSI](collegamento-csi.md) |
|
| 1 / 5 / 10 vittorie in campionato | partite vinte da dati CSI ufficiali | 1/5/10 | modulo [Collegamento CSI](collegamento-csi.md) |
|
||||||
| 1 evento di squadra al mese | eventi di tipo "evento" nel mese | 1 | `eventi_app` |
|
| 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente (dinamico) | 1 | `eventi_app` |
|
||||||
|
|
||||||
Mostrati in `squadra.tsx` (elenco completo con barra di progresso) e in `index.tsx` (home: il
|
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
|
primo obiettivo non completato). Un obiettivo che supera il 90% genera anche una notifica
|
||||||
@@ -46,9 +47,6 @@ smart (`notifiche-smart.ts`).
|
|||||||
[Serie di presenze](serie-presenze.md)), calcolato sui dati reali: un evento passato senza
|
[Serie di presenze](serie-presenze.md)), calcolato sui dati reali: un evento passato senza
|
||||||
risposta vale come assenza e azzera la serie, quindi l'obiettivo misura anche quanto la
|
risposta vale come assenza e azzera la serie, quindi l'obiettivo misura anche quanto la
|
||||||
squadra risponde alle convocazioni, non solo la presenza.
|
squadra risponde alle convocazioni, non solo la presenza.
|
||||||
- **Il mese di riferimento è una costante fissa nel codice** (agosto 2026): gli obiettivi
|
|
||||||
legati al mese corrente vanno aggiornati manualmente a ogni cambio di mese o stagione, oggi
|
|
||||||
sono "congelati" su un mese già passato.
|
|
||||||
- Le vittorie di campionato dipendono dal parsing HTML del portale CSI: se quel parsing si
|
- Le vittorie di campionato dipendono dal parsing HTML del portale CSI: se quel parsing si
|
||||||
rompe, questi tre obiettivi restano a 0% anche a fronte di vittorie reali.
|
rompe, questi tre obiettivi restano a 0% anche a fronte di vittorie reali.
|
||||||
- I target (250 presenze, 200 pagelle, ecc.) sono costanti fisse, da rivedere manualmente a
|
- I target (250 presenze, 200 pagelle, ecc.) sono costanti fisse, da rivedere manualmente a
|
||||||
@@ -56,6 +54,13 @@ smart (`notifiche-smart.ts`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Evoluzioni possibili
|
## Obiettivi mensili — mese dinamico
|
||||||
|
|
||||||
- Calcolare il mese di riferimento dinamicamente invece di una costante hardcoded.
|
"90% presenze del mese" e "1 evento di squadra al mese" si azzerano automaticamente a ogni
|
||||||
|
cambio mese: il mese di riferimento è calcolato dalla data corrente (fuso Europe/Rome), non più
|
||||||
|
una costante fissa. Per il primo, titolo ("90% di presenze ad agosto" / "a settembre" / ...) e
|
||||||
|
scadenza (ultimo giorno del mese) seguono di conseguenza. Per i test, `obiettiviSquadra`/
|
||||||
|
`obiettiviOrdinati` accettano un terzo parametro opzionale `oggi: Date` per iniettare una data
|
||||||
|
deterministica. Coperti sia da unit test (`test/unit/obiettivi.test.ts`, funzione pura) sia da
|
||||||
|
un integration test end-to-end (`test/integration/obiettivi.test.ts`, scrive/rilegge righe vere
|
||||||
|
su `eventi_app` e `risposte_presenze` sullo stack Supabase locale).
|
||||||
|
|||||||
+32
-9
@@ -26,11 +26,32 @@ export type ContestoObiettivi = {
|
|||||||
|
|
||||||
export const contestoVuoto: ContestoObiettivi = { eventi: [], presenze: {}, pagelle: [] };
|
export const contestoVuoto: ContestoObiettivi = { eventi: [], presenze: {}, pagelle: [] };
|
||||||
|
|
||||||
const MESE = "2026-08";
|
/** Mese corrente in formato "YYYY-MM" (fuso Europe/Rome), per gli obiettivi che si azzerano ogni mese. */
|
||||||
|
function meseCorrente(oggi: Date): string {
|
||||||
|
return new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(oggi).slice(0, 7);
|
||||||
|
}
|
||||||
|
|
||||||
function percentualePresenzeMese(ctx: ContestoObiettivi, rosaSize: number) {
|
/** "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(
|
||||||
|
oggi,
|
||||||
|
);
|
||||||
|
const preposizione = /^[aeiou]/i.test(nome) ? "ad" : "a";
|
||||||
|
return `${preposizione} ${nome}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ultimo giorno del mese corrente, come "YYYY-MM-DD". */
|
||||||
|
function fineMese(oggi: Date): string {
|
||||||
|
const mese = meseCorrente(oggi);
|
||||||
|
const anno = Number(mese.slice(0, 4));
|
||||||
|
const numeroMese = Number(mese.slice(5, 7));
|
||||||
|
const ultimoGiorno = new Date(Date.UTC(anno, numeroMese, 0)).getUTCDate();
|
||||||
|
return `${mese}-${String(ultimoGiorno).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentualePresenzeMese(ctx: ContestoObiettivi, rosaSize: number, mese: string) {
|
||||||
const delMese = ctx.eventi.filter(
|
const delMese = ctx.eventi.filter(
|
||||||
(e) => e.data.startsWith(MESE) && (e.tipo === "partita" || e.tipo === "allenamento"),
|
(e) => e.data.startsWith(mese) && (e.tipo === "partita" || e.tipo === "allenamento"),
|
||||||
);
|
);
|
||||||
if (delMese.length === 0 || rosaSize === 0) return 0;
|
if (delMese.length === 0 || rosaSize === 0) return 0;
|
||||||
const posti = delMese.length * rosaSize;
|
const posti = delMese.length * rosaSize;
|
||||||
@@ -56,19 +77,21 @@ function percentualeRisposte(ctx: ContestoObiettivi, rosaSize: number) {
|
|||||||
export function obiettiviSquadra(
|
export function obiettiviSquadra(
|
||||||
rosa: Giocatore[] = giocatori,
|
rosa: Giocatore[] = giocatori,
|
||||||
ctx: ContestoObiettivi = contestoVuoto,
|
ctx: ContestoObiettivi = contestoVuoto,
|
||||||
|
oggi: Date = new Date(),
|
||||||
): ObiettivoSquadra[] {
|
): ObiettivoSquadra[] {
|
||||||
const somma = (f: (g: Giocatore) => number) => rosa.reduce((s, g) => s + f(g), 0);
|
const somma = (f: (g: Giocatore) => number) => rosa.reduce((s, g) => s + f(g), 0);
|
||||||
const continui = rosa.filter((g) => g.serieAllenamenti >= 3).length;
|
const continui = rosa.filter((g) => g.serieAllenamenti >= 3).length;
|
||||||
const vittorie = ctx.vittorie ?? 0;
|
const vittorie = ctx.vittorie ?? 0;
|
||||||
|
const mese = meseCorrente(oggi);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "o1",
|
id: "o1",
|
||||||
titolo: "90% di presenze ad agosto",
|
titolo: `90% di presenze ${aMese(oggi)}`,
|
||||||
descrizione: "Media presenze su partite e allenamenti del mese",
|
descrizione: "Media presenze su partite e allenamenti del mese",
|
||||||
valore: percentualePresenzeMese(ctx, rosa.length),
|
valore: percentualePresenzeMese(ctx, rosa.length, mese),
|
||||||
target: 90,
|
target: 90,
|
||||||
unita: "%",
|
unita: "%",
|
||||||
scadenza: "2026-08-31",
|
scadenza: fineMese(oggi),
|
||||||
emoji: "📣",
|
emoji: "📣",
|
||||||
impatto: "Ogni sì in più alza la media di tutta la squadra.",
|
impatto: "Ogni sì in più alza la media di tutta la squadra.",
|
||||||
},
|
},
|
||||||
@@ -157,7 +180,7 @@ export function obiettiviSquadra(
|
|||||||
id: "o6",
|
id: "o6",
|
||||||
titolo: "1 evento di squadra al mese",
|
titolo: "1 evento di squadra al mese",
|
||||||
descrizione: "Pizzate, cene e uscite fuori dal campo",
|
descrizione: "Pizzate, cene e uscite fuori dal campo",
|
||||||
valore: ctx.eventi.filter((e) => e.tipo === "evento" && e.data.startsWith(MESE)).length,
|
valore: ctx.eventi.filter((e) => e.tipo === "evento" && e.data.startsWith(mese)).length,
|
||||||
target: 1,
|
target: 1,
|
||||||
unita: "eventi",
|
unita: "eventi",
|
||||||
emoji: "🍕",
|
emoji: "🍕",
|
||||||
@@ -170,8 +193,8 @@ export function progressoObiettivo(o: ObiettivoSquadra) {
|
|||||||
return Math.min(100, Math.round((o.valore / o.target) * 100));
|
return Math.min(100, Math.round((o.valore / o.target) * 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function obiettiviOrdinati(rosa?: Giocatore[], ctx?: ContestoObiettivi) {
|
export function obiettiviOrdinati(rosa?: Giocatore[], ctx?: ContestoObiettivi, oggi?: Date) {
|
||||||
return obiettiviSquadra(rosa, ctx).sort((a, b) => {
|
return obiettiviSquadra(rosa, ctx, oggi).sort((a, b) => {
|
||||||
const pa = progressoObiettivo(a);
|
const pa = progressoObiettivo(a);
|
||||||
const pb = progressoObiettivo(b);
|
const pb = progressoObiettivo(b);
|
||||||
const ca = pa >= 100 ? 1 : 0;
|
const ca = pa >= 100 ? 1 : 0;
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Obiettivi mensili di squadra ("presenze del mese" e "evento di squadra al mese")
|
||||||
|
* end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`.
|
||||||
|
*
|
||||||
|
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
|
||||||
|
* pura, con un `ContestoObiettivi` costruito a mano. Qui invece si scrivono righe vere su
|
||||||
|
* `eventi_app` e `risposte_presenze`, si rileggono con `leggiEventi()` (la stessa funzione che
|
||||||
|
* usa l'app lato server) e una query REST equivalente a `fetchPresenze()`, e si verifica che il
|
||||||
|
* risultato che arriva a `obiettiviSquadra()` sia quello atteso: se una colonna cambia nome o
|
||||||
|
* la mappatura si rompe, qui il valore torna sbagliato (o NaN) anche se i test unitari restano
|
||||||
|
* verdi, perché quelli non toccano mai il database.
|
||||||
|
*
|
||||||
|
* Il mese di test è marzo 2099: non collide con gli eventi del seed (tutti in agosto 2026), così
|
||||||
|
* questi due obiettivi — che ora aggregano su TUTTI gli eventi del mese corrente, non solo
|
||||||
|
* quelli del test — restano prevedibili senza dover filtrare gli eventi letti dal database.
|
||||||
|
*
|
||||||
|
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||||
|
* usa id con il prefisso `test-obiettivi`, che nessun dato vero può avere.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { giocatori } from "@/lib/crapp-data";
|
||||||
|
import { obiettiviSquadra } from "@/lib/obiettivi";
|
||||||
|
import type { MappaPresenze } from "@/lib/presenze";
|
||||||
|
import { statoLocale } from "../helpers/locale";
|
||||||
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
|
const locale = statoLocale();
|
||||||
|
|
||||||
|
if (!locale) {
|
||||||
|
salta("obiettivi (o1) sul database", "stack locale non attivo (npx supabase start)");
|
||||||
|
riepilogo("obiettivi");
|
||||||
|
} else {
|
||||||
|
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||||
|
console.log(`obiettivi su ${URL_BASE}`);
|
||||||
|
|
||||||
|
// Prima di qualsiasi import dei moduli server: è da qui che nasce `supabaseAdmin`.
|
||||||
|
process.env["SUPABASE_URL"] = URL_BASE;
|
||||||
|
process.env["SUPABASE_SERVICE_ROLE_KEY"] = SERVIZIO;
|
||||||
|
|
||||||
|
const { leggiEventi } = await import("@/lib/eventi.server");
|
||||||
|
|
||||||
|
const PREFISSO = "test-obiettivi";
|
||||||
|
const MESE_TEST = "2099-03";
|
||||||
|
const OGGI = new Date("2099-03-15T10:00:00Z");
|
||||||
|
|
||||||
|
const rest = (percorso: string, init?: RequestInit) =>
|
||||||
|
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
apikey: SERVIZIO,
|
||||||
|
Authorization: `Bearer ${SERVIZIO}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
...(init?.headers ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Stessa query di `fetchPresenze()` (`src/lib/presenze.ts`), raggruppata a mano. */
|
||||||
|
async function leggiPresenze(eventoId: string): Promise<MappaPresenze> {
|
||||||
|
const res = await rest(
|
||||||
|
`risposte_presenze?evento_id=eq.${eventoId}&select=evento_id,giocatore_id,stato`,
|
||||||
|
);
|
||||||
|
const righe = (await res.json()) as Array<{
|
||||||
|
evento_id: string;
|
||||||
|
giocatore_id: string;
|
||||||
|
stato: string;
|
||||||
|
}>;
|
||||||
|
const mappa: MappaPresenze = {};
|
||||||
|
for (const r of righe) {
|
||||||
|
(mappa[r.evento_id] ??= {})[r.giocatore_id] = r.stato as never;
|
||||||
|
}
|
||||||
|
return mappa;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventoId = `${PREFISSO}-e1`;
|
||||||
|
|
||||||
|
await prova("prepara evento e presenze reali su Supabase locale", async () => {
|
||||||
|
const inserito = await rest("eventi_app", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: eventoId,
|
||||||
|
tipo: "allenamento",
|
||||||
|
titolo: "Test obiettivi o1",
|
||||||
|
data: `${MESE_TEST}-10`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
||||||
|
|
||||||
|
// 9 presenti su 17 giocatori del roster reale (`crapp-data.ts`, id g1..g17).
|
||||||
|
const presenti = giocatori.slice(0, 9).map((g) => g.id);
|
||||||
|
const righe = giocatori.map((g) => ({
|
||||||
|
evento_id: eventoId,
|
||||||
|
giocatore_id: g.id,
|
||||||
|
stato: presenti.includes(g.id) ? "presente" : "assente",
|
||||||
|
}));
|
||||||
|
const inseritePresenze = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) });
|
||||||
|
if (!inseritePresenze.ok) {
|
||||||
|
throw new Error(`inserimento presenze fallito: ${await inseritePresenze.text()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova(
|
||||||
|
"o1 legge l'evento e le presenze vere dal database e calcola la percentuale del mese",
|
||||||
|
async () => {
|
||||||
|
const eventiReali = await leggiEventi();
|
||||||
|
assert.ok(
|
||||||
|
eventiReali.some((e) => e.id === eventoId),
|
||||||
|
"leggiEventi() include l'evento appena inserito",
|
||||||
|
);
|
||||||
|
const presenzeReali = await leggiPresenze(eventoId);
|
||||||
|
|
||||||
|
const obiettivi = obiettiviSquadra(
|
||||||
|
giocatori,
|
||||||
|
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
|
||||||
|
OGGI,
|
||||||
|
);
|
||||||
|
const o1 = obiettivi.find((o) => o.id === "o1")!;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
o1.valore,
|
||||||
|
Math.round((9 / giocatori.length) * 100),
|
||||||
|
"9 presenti su tutta la rosa, letti dal database",
|
||||||
|
);
|
||||||
|
assert.equal(o1.titolo, "90% di presenze a marzo", "titolo segue il mese iniettato");
|
||||||
|
assert.equal(o1.scadenza, "2099-03-31", "scadenza = ultimo giorno di marzo");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await prova("un evento fuori dal mese di test non sposta o1", async () => {
|
||||||
|
const eventoFuoriMese = `${PREFISSO}-e2`;
|
||||||
|
const fuori = await rest("eventi_app", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: eventoFuoriMese,
|
||||||
|
tipo: "allenamento",
|
||||||
|
titolo: "Test obiettivi o1 - fuori mese",
|
||||||
|
data: "2099-04-01",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!fuori.ok) throw new Error(`inserimento evento fallito: ${await fuori.text()}`);
|
||||||
|
try {
|
||||||
|
const presenzeFuoriMese = giocatori.map((g) => ({
|
||||||
|
evento_id: eventoFuoriMese,
|
||||||
|
giocatore_id: g.id,
|
||||||
|
stato: "presente",
|
||||||
|
}));
|
||||||
|
const scritte = await rest("risposte_presenze", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(presenzeFuoriMese),
|
||||||
|
});
|
||||||
|
if (!scritte.ok) throw new Error(`inserimento presenze fallito: ${await scritte.text()}`);
|
||||||
|
|
||||||
|
const eventiReali = await leggiEventi();
|
||||||
|
const presenzeReali = {
|
||||||
|
...(await leggiPresenze(eventoId)),
|
||||||
|
...(await leggiPresenze(eventoFuoriMese)),
|
||||||
|
};
|
||||||
|
const o1 = obiettiviSquadra(
|
||||||
|
giocatori,
|
||||||
|
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
|
||||||
|
OGGI,
|
||||||
|
).find((o) => o.id === "o1")!;
|
||||||
|
assert.equal(
|
||||||
|
o1.valore,
|
||||||
|
Math.round((9 / giocatori.length) * 100),
|
||||||
|
"l'evento di aprile non entra nel calcolo di marzo",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await rest(`eventi_app?id=eq.${eventoFuoriMese}`, { method: "DELETE" });
|
||||||
|
await rest(`risposte_presenze?evento_id=eq.${eventoFuoriMese}`, { method: "DELETE" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova(
|
||||||
|
"o6 legge dal database l'evento sociale del mese, azzerandosi come o1",
|
||||||
|
async () => {
|
||||||
|
const pizzataId = `${PREFISSO}-pizzata`;
|
||||||
|
const inserita = await rest("eventi_app", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: pizzataId,
|
||||||
|
tipo: "evento",
|
||||||
|
titolo: "Test obiettivi o6",
|
||||||
|
data: `${MESE_TEST}-20`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!inserita.ok) throw new Error(`inserimento evento fallito: ${await inserita.text()}`);
|
||||||
|
|
||||||
|
const eventiReali = await leggiEventi();
|
||||||
|
const o6InMese = obiettiviSquadra(
|
||||||
|
giocatori,
|
||||||
|
{ eventi: eventiReali, presenze: {}, pagelle: [] },
|
||||||
|
OGGI,
|
||||||
|
).find((o) => o.id === "o6")!;
|
||||||
|
assert.equal(o6InMese.valore, 1, "l'evento sociale di marzo conta letto dal database");
|
||||||
|
|
||||||
|
const OGGI_MESE_DOPO = new Date("2099-04-15T10:00:00Z");
|
||||||
|
const o6MeseDopo = obiettiviSquadra(
|
||||||
|
giocatori,
|
||||||
|
{ eventi: eventiReali, presenze: {}, pagelle: [] },
|
||||||
|
OGGI_MESE_DOPO,
|
||||||
|
).find((o) => o.id === "o6")!;
|
||||||
|
assert.equal(
|
||||||
|
o6MeseDopo.valore,
|
||||||
|
0,
|
||||||
|
"lo stesso evento non conta più il mese successivo (si azzera)",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||||
|
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
|
||||||
|
riepilogo("obiettivi");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,8 +30,12 @@ const evento = (id: string, data: string, tipo: Evento["tipo"]): Evento => ({
|
|||||||
|
|
||||||
const trova = (lista: ObiettivoSquadra[], id: string) => lista.find((o) => o.id === id)!;
|
const trova = (lista: ObiettivoSquadra[], id: string) => lista.find((o) => o.id === id)!;
|
||||||
|
|
||||||
|
// Data di riferimento fissa: o1 (presenze del mese) ora dipende dal mese corrente,
|
||||||
|
// quindi va iniettata esplicitamente per avere test deterministici.
|
||||||
|
const OGGI_AGOSTO = new Date("2026-08-15T10:00:00Z");
|
||||||
|
|
||||||
// --- contesto vuoto: nessuna divisione per zero ------------------------------
|
// --- contesto vuoto: nessuna divisione per zero ------------------------------
|
||||||
const vuoti = obiettiviSquadra(giocatori, contestoVuoto);
|
const vuoti = obiettiviSquadra(giocatori, contestoVuoto, OGGI_AGOSTO);
|
||||||
assert.equal(trova(vuoti, "o1").valore, 0, "nessun evento nel mese: 0%, non NaN");
|
assert.equal(trova(vuoti, "o1").valore, 0, "nessun evento nel mese: 0%, non NaN");
|
||||||
assert.equal(trova(vuoti, "o2").valore, 0);
|
assert.equal(trova(vuoti, "o2").valore, 0);
|
||||||
assert.equal(trova(vuoti, "o12").valore, 0, "nessuna pagella: media 0");
|
assert.equal(trova(vuoti, "o12").valore, 0, "nessuna pagella: media 0");
|
||||||
@@ -49,25 +53,32 @@ const ctx: ContestoObiettivi = {
|
|||||||
presenze: tuttiPresenti,
|
presenze: tuttiPresenti,
|
||||||
pagelle: [],
|
pagelle: [],
|
||||||
};
|
};
|
||||||
assert.equal(trova(obiettiviSquadra(giocatori, ctx), "o1").valore, 100, "rosa al completo = 100%");
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctx, OGGI_AGOSTO), "o1").valore,
|
||||||
|
100,
|
||||||
|
"rosa al completo = 100%",
|
||||||
|
);
|
||||||
|
|
||||||
const metaRosa: MappaPresenze = {
|
const metaRosa: MappaPresenze = {
|
||||||
a1: Object.fromEntries(
|
a1: Object.fromEntries(
|
||||||
giocatori.map((g, i) => [g.id, i % 2 === 0 ? ("presente" as const) : ("assente" as const)]),
|
giocatori.map((g, i) => [g.id, i % 2 === 0 ? ("presente" as const) : ("assente" as const)]),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
const percentuale = trova(obiettiviSquadra(giocatori, { ...ctx, presenze: metaRosa }), "o1").valore;
|
const percentuale = trova(
|
||||||
|
obiettiviSquadra(giocatori, { ...ctx, presenze: metaRosa }, OGGI_AGOSTO),
|
||||||
|
"o1",
|
||||||
|
).valore;
|
||||||
assert.ok(percentuale > 40 && percentuale < 60, `metà rosa presente ≈ 50%, era ${percentuale}`);
|
assert.ok(percentuale > 40 && percentuale < 60, `metà rosa presente ≈ 50%, era ${percentuale}`);
|
||||||
|
|
||||||
// Il ritardo conta come presenza, il "forse" no.
|
// Il ritardo conta come presenza, il "forse" no.
|
||||||
const conRitardo: MappaPresenze = { a1: { g1: "ritardo", g2: "forse" } };
|
const conRitardo: MappaPresenze = { a1: { g1: "ritardo", g2: "forse" } };
|
||||||
assert.equal(
|
assert.equal(
|
||||||
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }), "o1").valore,
|
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }, OGGI_AGOSTO), "o1").valore,
|
||||||
Math.round((1 / giocatori.length) * 100),
|
Math.round((1 / giocatori.length) * 100),
|
||||||
"solo il ritardo conta come presente",
|
"solo il ritardo conta come presente",
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }), "o2").valore,
|
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }, OGGI_AGOSTO), "o2").valore,
|
||||||
Math.round((2 / giocatori.length) * 100),
|
Math.round((2 / giocatori.length) * 100),
|
||||||
"per le risposte anche il forse conta",
|
"per le risposte anche il forse conta",
|
||||||
);
|
);
|
||||||
@@ -78,7 +89,7 @@ const fuoriMese: ContestoObiettivi = {
|
|||||||
presenze: { s1: { g1: "presente" } },
|
presenze: { s1: { g1: "presente" } },
|
||||||
pagelle: [],
|
pagelle: [],
|
||||||
};
|
};
|
||||||
assert.equal(trova(obiettiviSquadra(giocatori, fuoriMese), "o1").valore, 0);
|
assert.equal(trova(obiettiviSquadra(giocatori, fuoriMese, OGGI_AGOSTO), "o1").valore, 0);
|
||||||
|
|
||||||
// I compleanni non richiedono risposta.
|
// I compleanni non richiedono risposta.
|
||||||
const soloCompleanni: ContestoObiettivi = {
|
const soloCompleanni: ContestoObiettivi = {
|
||||||
@@ -86,7 +97,68 @@ const soloCompleanni: ContestoObiettivi = {
|
|||||||
presenze: {},
|
presenze: {},
|
||||||
pagelle: [],
|
pagelle: [],
|
||||||
};
|
};
|
||||||
assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni), "o2").valore, 0);
|
assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2").valore, 0);
|
||||||
|
|
||||||
|
// --- o1: si azzera a ogni cambio mese, in base agli eventi a calendario ------
|
||||||
|
{
|
||||||
|
// Stesso evento/presenze: "in mese" a settembre, "fuori mese" se letto da agosto.
|
||||||
|
const OGGI_SETTEMBRE = new Date("2026-09-05T10:00:00Z");
|
||||||
|
const eventoSettembre: ContestoObiettivi = {
|
||||||
|
eventi: [evento("s2", "2026-09-04", "allenamento")],
|
||||||
|
presenze: { s2: Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])) },
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, eventoSettembre, OGGI_SETTEMBRE), "o1").valore,
|
||||||
|
100,
|
||||||
|
"evento di settembre conta se oggi è settembre",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, eventoSettembre, OGGI_AGOSTO), "o1").valore,
|
||||||
|
0,
|
||||||
|
"lo stesso evento non conta se oggi è agosto",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Titolo e scadenza seguono il mese corrente, non più una stagione fissa.
|
||||||
|
const o1Agosto = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_AGOSTO), "o1");
|
||||||
|
assert.equal(o1Agosto.titolo, "90% di presenze ad agosto", "elisione 'ad' davanti a vocale");
|
||||||
|
assert.equal(o1Agosto.scadenza, "2026-08-31", "scadenza = ultimo giorno del mese");
|
||||||
|
|
||||||
|
const o1Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o1");
|
||||||
|
assert.ok(o1Settembre.titolo.includes("settembre"), "titolo o1 riflette il mese iniettato (settembre)");
|
||||||
|
assert.equal(o1Settembre.scadenza, "2026-09-30", "scadenza = ultimo giorno di settembre (30 gg)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- o6: evento di squadra al mese, si azzera come o1 ------------------------
|
||||||
|
{
|
||||||
|
const OGGI_SETTEMBRE = new Date("2026-09-05T10:00:00Z");
|
||||||
|
const pizzataSettembre: ContestoObiettivi = {
|
||||||
|
eventi: [evento("p1", "2026-09-12", "evento")],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, pizzataSettembre, OGGI_SETTEMBRE), "o6").valore,
|
||||||
|
1,
|
||||||
|
"l'evento sociale di settembre conta se oggi è settembre",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, pizzataSettembre, OGGI_AGOSTO), "o6").valore,
|
||||||
|
0,
|
||||||
|
"lo stesso evento non conta se oggi è agosto",
|
||||||
|
);
|
||||||
|
|
||||||
|
const allenamentoNelMese: ContestoObiettivi = {
|
||||||
|
eventi: [evento("al1", "2026-08-12", "allenamento")],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, allenamentoNelMese, OGGI_AGOSTO), "o6").valore,
|
||||||
|
0,
|
||||||
|
"un allenamento nel mese non è un evento sociale",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// --- somme sulla rosa --------------------------------------------------------
|
// --- somme sulla rosa --------------------------------------------------------
|
||||||
const sommaPresenze = giocatori.reduce((s, g) => s + g.presenze, 0);
|
const sommaPresenze = giocatori.reduce((s, g) => s + g.presenze, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user