Obiettivo Presenze collettive: percentuale al 90% su allenamenti e partite definiti
Sostituisce «250 presenze complessive». Il valore è la percentuale di presenze (presente o ritardo) su tutti gli allenamenti e le partite in calendario, passati e futuri, con i convocati come posti (tutta la rosa se non ce ne sono). Stagione intera, nessun reset mensile. Test unitari ampliati (arrotondamento, convocati, ora legale, cambio mese) e test di integrazione di o6 riallineato alla regola dell'ora dell'evento. Documentazione e CHANGELOG aggiornati. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff02b7439c
commit
6b8cc33512
5 files changed
+199
-64
No files matched your search
@@ -24,6 +24,8 @@ versioni `0.y.z` erano pre-release.
|
|||||||
|
|
||||||
- **Obiettivo «1 evento di squadra al mese»** — si completa quando arriva l'ora dell'evento, non appena viene
|
- **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.
|
creato. Un evento futuro non conta finché la sua data e ora non sono passate. Nessuna migration.
|
||||||
|
- **Obiettivo «Presenze collettive»** — sostituisce «250 presenze complessive»: è la percentuale di presenze di tutta la
|
||||||
|
rosa su allenamenti e partite definiti in stagione (passati e futuri, posti = convocati), con target 90%. Nessuna migration.
|
||||||
|
|
||||||
## [1.2.3] - 2026-10-04
|
## [1.2.3] - 2026-10-04
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ corrente dinamico (vedi sotto).
|
|||||||
## Obiettivi definiti
|
## Obiettivi definiti
|
||||||
|
|
||||||
| id | Obiettivo | Calcolo | Target | Fonte |
|
| id | Obiettivo | Calcolo | Target | Fonte |
|
||||||
| ----- | ---------------------------------- | ----------------------------------------------------------------------------- | --------------------- | ----------------------------- |
|
| ----- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------- |
|
||||||
| `o1` | 90% presenze del mese | risposte presente/ritardo su partite+allenamenti del mese corrente (dinamico) | 90% | `risposte_presenze` |
|
| `o1` | 90% presenze del mese | risposte presente/ritardo su partite+allenamenti del mese corrente (dinamico) | 90% | `risposte_presenze` |
|
||||||
| `o2` | Tutti rispondono alle convocazioni | risposte totali / eventi possibili (esclusi i compleanni) | 90% | `risposte_presenze` |
|
| `o2` | Tutti rispondono alle convocazioni | risposte totali / eventi possibili (esclusi i compleanni) | 90% | `risposte_presenze` |
|
||||||
| `o7` | 250 presenze complessive | somma presenze di tutta la rosa, stagione intera | 250 | aggregato da `useRosa()` |
|
| `o7` | Presenze collettive | % di presenze (presente/ritardo) su tutti gli allenamenti e le partite definiti, passati e futuri; posti = convocati dell'evento (tutta la rosa se vuoto) | 90 | `eventi_app` + `risposte_presenze` |
|
||||||
| `o12` | Media pagelle da 7.5 | media di tutti i voti, arrotondata a una cifra decimale | 7.5 | `pagelle_voti` |
|
| `o12` | Media pagelle da 7.5 | media di tutti i voti, arrotondata a una cifra decimale | 7.5 | `pagelle_voti` |
|
||||||
| `o13` | 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
| `o13` | 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
||||||
| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` |
|
| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` |
|
||||||
@@ -52,7 +52,7 @@ corrente dinamico (vedi sotto).
|
|||||||
|
|
||||||
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
|
||||||
smart (`notifiche-smart.ts`). I target fissi (250 presenze, 200 pagelle, 7.5 di media, 1/5/10
|
smart (`notifiche-smart.ts`). I target fissi (200 pagelle, 7.5 di media, 1/5/10
|
||||||
vittorie) sono scelte editoriali da rivedere a mano a ogni stagione — nessuna configurazione o
|
vittorie) sono scelte editoriali da rivedere a mano a ogni stagione — nessuna configurazione o
|
||||||
UI per farlo, si cambia il numero in `obiettivi.ts`. Fa eccezione "Continuità di squadra"
|
UI per farlo, si cambia il numero in `obiettivi.ts`. Fa eccezione "Continuità di squadra"
|
||||||
(vedi sotto): il suo target ha un significato specifico, non va scalato come gli altri.
|
(vedi sotto): il suo target ha un significato specifico, non va scalato come gli altri.
|
||||||
@@ -73,6 +73,12 @@ un evento di tipo "evento" del mese corrente il cui `data` + `ora` (fuso Europe/
|
|||||||
0/1 finché non scatta l'ora. Il valore si ricalcola al render: con l'app già aperta il
|
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.
|
passaggio a completato compare al refresh successivo.
|
||||||
|
|
||||||
|
`o7` ("Presenze collettive") invece **non** si azzera: copre tutta la stagione, cioè tutti gli
|
||||||
|
allenamenti e le partite in calendario, passati e futuri (le risposte sugli eventi futuri sono
|
||||||
|
conferme). Differisce da `o1` per periodo (stagione, non mese) e perché i posti sono i convocati
|
||||||
|
di ciascun evento, non sempre l'intera rosa. Non c'è un confine di stagione esplicito: conta ciò
|
||||||
|
che c'è in `eventi_app`.
|
||||||
|
|
||||||
`o2` ("Tutti rispondono alle convocazioni") non si azzera — aggrega su tutti gli eventi in
|
`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 è
|
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.
|
anch'essa l'ultimo giorno del mese corrente (`fineMese(oggi)`), non più una data fissa.
|
||||||
@@ -163,8 +169,8 @@ da un backend reale, non solo funzione pura con contesto costruito a mano).
|
|||||||
query REST equivalente a `fetchPresenze()`. Copre: contesto vuoto, aggregazione su più eventi,
|
query REST equivalente a `fetchPresenze()`. Copre: contesto vuoto, aggregazione su più eventi,
|
||||||
filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico
|
filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico
|
||||||
(evento dentro/fuori mese), scadenza dinamica.
|
(evento dentro/fuori mese), scadenza dinamica.
|
||||||
- **o7** (Supabase locale): scrive eventi/presenze reali, calcola `contaPresenzeGiocatore()` (la
|
- **o7** (Supabase locale): scrive un allenamento e una partita con convocati e presenze reali,
|
||||||
stessa funzione pura usata da `useRosa()` in produzione) sui dati riletti, verifica la somma.
|
li rilegge con `leggiEventi()`/`leggiPresenze()` e verifica la percentuale (il ritardo conta).
|
||||||
- **o11** (Supabase locale): scrive tre allenamenti e presenze reali, calcola
|
- **o11** (Supabase locale): scrive tre allenamenti e presenze reali, calcola
|
||||||
`serieConsecutiva()` sui dati riletti, verifica che solo chi resta in serie venga contato.
|
`serieConsecutiva()` sui dati riletti, verifica che solo chi resta in serie venga contato.
|
||||||
- **o12/o13** (Supabase locale): scrive voti veri su `pagelle_voti` rispettando i vincoli reali
|
- **o12/o13** (Supabase locale): scrive voti veri su `pagelle_voti` rispettando i vincoli reali
|
||||||
|
|||||||
+26
-6
@@ -80,6 +80,27 @@ function percentualePresenzeMese(ctx: ContestoObiettivi, rosaSize: number, mese:
|
|||||||
return Math.round((presenti / posti) * 100);
|
return Math.round((presenti / posti) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presenze (presente o ritardo) su tutti gli allenamenti e le partite definiti, passati e
|
||||||
|
* futuri, sul totale dei posti: per ogni evento i convocati, o tutta la rosa se non ce ne sono.
|
||||||
|
*/
|
||||||
|
function percentualePresenzeCollettive(ctx: ContestoObiettivi, rosa: Giocatore[]) {
|
||||||
|
let posti = 0;
|
||||||
|
let presenti = 0;
|
||||||
|
for (const e of ctx.eventi) {
|
||||||
|
if (e.tipo !== "partita" && e.tipo !== "allenamento") continue;
|
||||||
|
const attesi = e.convocati.length === 0 ? rosa : rosa.filter((g) => e.convocati.includes(g.id));
|
||||||
|
posti += attesi.length;
|
||||||
|
const risposte = ctx.presenze[e.id] ?? {};
|
||||||
|
presenti += attesi.filter((g) => {
|
||||||
|
const stato = risposte[g.id];
|
||||||
|
return stato === "presente" || stato === "ritardo";
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
if (posti === 0) return 0;
|
||||||
|
return Math.round((presenti / posti) * 100);
|
||||||
|
}
|
||||||
|
|
||||||
function percentualeRisposte(ctx: ContestoObiettivi, rosaSize: number) {
|
function percentualeRisposte(ctx: ContestoObiettivi, rosaSize: number) {
|
||||||
const daRispondere = ctx.eventi.filter((e) => e.tipo !== "compleanno");
|
const daRispondere = ctx.eventi.filter((e) => e.tipo !== "compleanno");
|
||||||
if (daRispondere.length === 0 || rosaSize === 0) return 0;
|
if (daRispondere.length === 0 || rosaSize === 0) return 0;
|
||||||
@@ -97,7 +118,6 @@ export function obiettiviSquadra(
|
|||||||
ctx: ContestoObiettivi = contestoVuoto,
|
ctx: ContestoObiettivi = contestoVuoto,
|
||||||
oggi: Date = new Date(),
|
oggi: Date = new Date(),
|
||||||
): ObiettivoSquadra[] {
|
): ObiettivoSquadra[] {
|
||||||
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);
|
const mese = meseCorrente(oggi);
|
||||||
@@ -126,11 +146,11 @@ export function obiettiviSquadra(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o7",
|
id: "o7",
|
||||||
titolo: "250 presenze complessive",
|
titolo: "Presenze collettive",
|
||||||
descrizione: "Somma delle presenze di tutta la rosa in stagione",
|
descrizione: "Presenze di tutta la rosa su allenamenti e partite definiti in stagione",
|
||||||
valore: somma((g) => g.presenze),
|
valore: percentualePresenzeCollettive(ctx, rosa),
|
||||||
target: 250,
|
target: 90,
|
||||||
unita: "presenze",
|
unita: "%",
|
||||||
emoji: "🤝",
|
emoji: "🤝",
|
||||||
impatto: "Ogni allenamento a cui vieni vale +1 per il gruppo.",
|
impatto: "Ogni allenamento a cui vieni vale +1 per il gruppo.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Obiettivi di squadra end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`.
|
* Obiettivi di squadra end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`.
|
||||||
* Copre gli obiettivi con scadenza/mese dinamici ("presenze del mese", "evento di squadra al
|
* Copre gli obiettivi con scadenza/mese dinamici ("presenze del mese", "evento di squadra al
|
||||||
* mese", "tutti rispondono alle convocazioni") e quelli la cui logica dipende da dati scritti
|
* mese", "tutti rispondono alle convocazioni") e quelli la cui logica dipende da dati scritti
|
||||||
* su altre tabelle ("250 presenze complessive" via `contaPresenzeGiocatore()`, "media pagelle
|
* su altre tabelle ("presenze collettive" via `leggiEventi()`/`leggiPresenze()`, "media pagelle
|
||||||
* da 7.5" e "200 pagelle compilate" via `pagelle_voti`).
|
* da 7.5" e "200 pagelle compilate" via `pagelle_voti`).
|
||||||
*
|
*
|
||||||
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
|
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { giocatori } from "@/lib/crapp-data";
|
import { giocatori } from "@/lib/crapp-data";
|
||||||
import { obiettiviSquadra } from "@/lib/obiettivi";
|
import { obiettiviSquadra } from "@/lib/obiettivi";
|
||||||
import { contaPresenzeGiocatore, serieConsecutiva, type MappaPresenze } from "@/lib/presenze";
|
import { serieConsecutiva, type MappaPresenze } from "@/lib/presenze";
|
||||||
import { statoLocale } from "../helpers/locale";
|
import { statoLocale } from "../helpers/locale";
|
||||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
@@ -282,33 +282,51 @@ if (!locale) {
|
|||||||
"o6 legge dal database l'evento sociale del mese, azzerandosi come o1",
|
"o6 legge dal database l'evento sociale del mese, azzerandosi come o1",
|
||||||
async () => {
|
async () => {
|
||||||
const pizzataId = `${PREFISSO}-pizzata`;
|
const pizzataId = `${PREFISSO}-pizzata`;
|
||||||
|
const pizzataFuturaId = `${PREFISSO}-pizzata-futura`;
|
||||||
|
// OGGI è il 15 marzo 12:00 a Roma: la pizzata del 10 è passata, quella del 20 no.
|
||||||
|
for (const [id, giorno] of [
|
||||||
|
[pizzataId, "10"],
|
||||||
|
[pizzataFuturaId, "20"],
|
||||||
|
] as const) {
|
||||||
const inserita = await rest("eventi_app", {
|
const inserita = await rest("eventi_app", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: pizzataId,
|
id,
|
||||||
tipo: "evento",
|
tipo: "evento",
|
||||||
titolo: "Test obiettivi o6",
|
titolo: "Test obiettivi o6",
|
||||||
data: `${MESE_TEST}-20`,
|
data: `${MESE_TEST}-${giorno}`,
|
||||||
|
ora: "20:30",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!inserita.ok) throw new Error(`inserimento evento fallito: ${await inserita.text()}`);
|
if (!inserita.ok) throw new Error(`inserimento evento fallito: ${await inserita.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
const eventiReali = await leggiEventi();
|
const tutti = await leggiEventi();
|
||||||
const o6InMese = obiettiviSquadra(
|
const o6Di = (ids: string[], oggi: Date) =>
|
||||||
|
obiettiviSquadra(
|
||||||
giocatori,
|
giocatori,
|
||||||
{ eventi: eventiReali, presenze: {}, pagelle: [] },
|
{ eventi: tutti.filter((e) => ids.includes(e.id)), presenze: {}, pagelle: [] },
|
||||||
OGGI,
|
oggi,
|
||||||
).find((o) => o.id === "o6")!;
|
).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(
|
assert.equal(
|
||||||
o6MeseDopo.valore,
|
o6Di([pizzataId], OGGI).valore,
|
||||||
|
1,
|
||||||
|
"l'evento sociale già passato conta, letto dal database",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
o6Di([pizzataFuturaId], OGGI).valore,
|
||||||
|
0,
|
||||||
|
"l'evento sociale futuro non conta finché non arriva la sua ora",
|
||||||
|
);
|
||||||
|
// Il 20 marzo 2099 alle 20:30 Roma (CET, UTC+1) = 19:30Z: da lì conta.
|
||||||
|
assert.equal(
|
||||||
|
o6Di([pizzataFuturaId], new Date("2099-03-20T19:30:00Z")).valore,
|
||||||
|
1,
|
||||||
|
"arrivata l'ora dell'evento conta",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
o6Di([pizzataId], new Date("2099-04-15T10:00:00Z")).valore,
|
||||||
0,
|
0,
|
||||||
"lo stesso evento non conta più il mese successivo (si azzera)",
|
"lo stesso evento non conta più il mese successivo (si azzera)",
|
||||||
);
|
);
|
||||||
@@ -386,14 +404,14 @@ if (!locale) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await prova("o7 somma presenze calcolate da eventi/risposte reali del database", async () => {
|
await prova(
|
||||||
// o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da
|
"o7 percentuale di presenze collettive da eventi/risposte reali del database",
|
||||||
// `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita
|
async () => {
|
||||||
// la stessa funzione pura sui dati appena scritti, per verificare l'intera catena
|
// o7 è la % di presenze su allenamenti e partite definiti (passati e futuri), con i
|
||||||
// DB -> contaPresenzeGiocatore -> o7, non solo la somma finale.
|
// convocati come denominatore. Si esercita l'intera catena DB -> leggiEventi/leggiPresenze -> o7.
|
||||||
const allenamentoId = `${PREFISSO}-o7-allenamento`;
|
const allenamentoId = `${PREFISSO}-o7-allenamento`;
|
||||||
const partitaId = `${PREFISSO}-o7-partita`;
|
const partitaId = `${PREFISSO}-o7-partita`;
|
||||||
const OGGI_STR = "2099-01-20";
|
const [g1, g2, g3] = giocatori;
|
||||||
|
|
||||||
for (const [id, tipo, data] of [
|
for (const [id, tipo, data] of [
|
||||||
[allenamentoId, "allenamento", "2099-01-05"],
|
[allenamentoId, "allenamento", "2099-01-05"],
|
||||||
@@ -401,14 +419,19 @@ if (!locale) {
|
|||||||
] as const) {
|
] as const) {
|
||||||
const inserito = await rest("eventi_app", {
|
const inserito = await rest("eventi_app", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }),
|
body: JSON.stringify({
|
||||||
|
id,
|
||||||
|
tipo,
|
||||||
|
titolo: `Test obiettivi o7 (${tipo})`,
|
||||||
|
data,
|
||||||
|
convocati: [g1!.id, g2!.id, g3!.id],
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze,
|
// g1 presente a entrambi, g2 presente e in ritardo (il ritardo conta), g3 assente a
|
||||||
// il ritardo conta). g3: assente a entrambi (0 presenze).
|
// entrambi: 4 presenze su 6 posti (3 convocati x 2 eventi) = 67%.
|
||||||
const [g1, g2, g3] = giocatori;
|
|
||||||
const righe = [
|
const righe = [
|
||||||
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
|
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
|
||||||
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
|
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
|
||||||
@@ -431,23 +454,19 @@ if (!locale) {
|
|||||||
...(await leggiPresenze(partitaId)),
|
...(await leggiPresenze(partitaId)),
|
||||||
};
|
};
|
||||||
|
|
||||||
const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({
|
const o7 = obiettiviSquadra([g1!, g2!, g3!], {
|
||||||
...g,
|
eventi: eventiReali,
|
||||||
presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR),
|
presenze: presenzeReali,
|
||||||
}));
|
|
||||||
|
|
||||||
const o7 = obiettiviSquadra(rosaConPresenzeReali, {
|
|
||||||
eventi: [],
|
|
||||||
presenze: {},
|
|
||||||
pagelle: [],
|
pagelle: [],
|
||||||
}).find((o) => o.id === "o7")!;
|
}).find((o) => o.id === "o7")!;
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
o7.valore,
|
o7.valore,
|
||||||
4,
|
67,
|
||||||
"g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database",
|
"4 presenze su 6 posti (il ritardo conta), calcolate dal database",
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
await prova("o12/o13 media e conteggio pagelle vere lette da pagelle_voti", async () => {
|
await prova("o12/o13 media e conteggio pagelle vere lette da pagelle_voti", async () => {
|
||||||
const matchId = `${PREFISSO}-o12-m1`;
|
const matchId = `${PREFISSO}-o12-m1`;
|
||||||
|
|||||||
+104
-16
@@ -274,6 +274,32 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
|
|||||||
0,
|
0,
|
||||||
"evento senza ora: la sera prima (Roma) non conta",
|
"evento senza ora: la sera prima (Roma) non conta",
|
||||||
);
|
);
|
||||||
|
// Ora legale: il 20/09 vale CEST (UTC+2), il 20/12 CET (UTC+1): 20:30 Roma = 18:30Z / 19:30Z.
|
||||||
|
const inverno = { ...evento("iv1", "2026-12-20", "evento"), ora: "20:30" };
|
||||||
|
const ctxInverno: ContestoObiettivi = { eventi: [inverno], presenze: {}, pagelle: [] };
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxInverno, new Date("2026-12-20T19:29:00Z")), "o6").valore,
|
||||||
|
0,
|
||||||
|
"inverno: 19:29Z = 20:29 a Roma, non ancora",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxInverno, new Date("2026-12-20T19:30:00Z")), "o6").valore,
|
||||||
|
1,
|
||||||
|
"inverno: 19:30Z = 20:30 a Roma, conta",
|
||||||
|
);
|
||||||
|
// Cambio mese a mezzanotte di Roma: 30/09 23:30Z è già 01/10 a Roma (CEST).
|
||||||
|
const fineSett = { ...evento("fs1", "2026-09-30", "evento"), ora: "20:00" };
|
||||||
|
const ctxFine: ContestoObiettivi = { eventi: [fineSett], presenze: {}, pagelle: [] };
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxFine, new Date("2026-09-30T21:59:00Z")), "o6").valore,
|
||||||
|
1,
|
||||||
|
"30/09 23:59 a Roma: ancora settembre, conta",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxFine, new Date("2026-09-30T22:00:00Z")), "o6").valore,
|
||||||
|
0,
|
||||||
|
"01/10 00:00 a Roma: nuovo mese, si azzera",
|
||||||
|
);
|
||||||
// Più eventi: ne basta uno passato; quello futuro non si somma.
|
// Più eventi: ne basta uno passato; quello futuro non si somma.
|
||||||
const misti: ContestoObiettivi = {
|
const misti: ContestoObiettivi = {
|
||||||
eventi: [evento("m1", "2026-09-05", "evento"), evento("m2", "2026-09-28", "evento")],
|
eventi: [evento("m1", "2026-09-05", "evento"), evento("m2", "2026-09-28", "evento")],
|
||||||
@@ -298,23 +324,85 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- somme sulla rosa --------------------------------------------------------
|
// --- o7: presenze collettive (% su allenamenti e partite definiti, passati e futuri) ---
|
||||||
const sommaPresenze = giocatori.reduce((s, g) => s + g.presenze, 0);
|
{
|
||||||
assert.equal(trova(vuoti, "o7").valore, sommaPresenze);
|
const [g1, g2, g3, g4] = giocatori;
|
||||||
assert.equal(
|
const rosa4 = [g1!, g2!, g3!, g4!];
|
||||||
trova(obiettiviSquadra([], contestoVuoto), "o7").valore,
|
const o7 = (c: ContestoObiettivi, r = rosa4) => trova(obiettiviSquadra(r, c, OGGI_AGOSTO), "o7");
|
||||||
0,
|
|
||||||
"rosa vuota: nessuna presenza",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Valori noti e indipendenti dai dati reali della rosa: non solo la stessa formula
|
assert.equal(o7(contestoVuoto).titolo, "Presenze collettive");
|
||||||
// ricalcolata sugli stessi dati, ma una somma verificabile a mente (5 + 10 + 15 = 30).
|
assert.equal(o7(contestoVuoto).target, 90);
|
||||||
const rosaControllata = giocatori.slice(0, 3).map((g, i) => ({ ...g, presenze: [5, 10, 15][i]! }));
|
assert.equal(o7(contestoVuoto).valore, 0, "nessun evento: 0, non NaN");
|
||||||
assert.equal(
|
assert.equal(o7({ ...contestoVuoto }, []).valore, 0, "rosa vuota: 0");
|
||||||
trova(obiettiviSquadra(rosaControllata, contestoVuoto), "o7").valore,
|
|
||||||
30,
|
// Allenamento passato (3 presenti su 4) + partita futura (1 presente su 4): 4 su 8 = 50%.
|
||||||
"somma di presenze note, indipendente dal roster reale",
|
const base: ContestoObiettivi = {
|
||||||
);
|
eventi: [evento("c1", "2026-07-01", "allenamento"), evento("c2", "2026-12-01", "partita")],
|
||||||
|
presenze: {
|
||||||
|
c1: { [g1!.id]: "presente", [g2!.id]: "ritardo", [g3!.id]: "presente", [g4!.id]: "assente" },
|
||||||
|
c2: { [g1!.id]: "presente", [g2!.id]: "assente" },
|
||||||
|
},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(o7(base).valore, 50, "passati e futuri nel denominatore, ritardo conta");
|
||||||
|
|
||||||
|
// Con convocati il denominatore sono solo loro: c1 per g1 e g2 (2 presenti su 2) = 100%.
|
||||||
|
const conConvocati: ContestoObiettivi = {
|
||||||
|
...base,
|
||||||
|
eventi: [{ ...evento("c1", "2026-07-01", "allenamento"), convocati: [g1!.id, g2!.id] }],
|
||||||
|
};
|
||||||
|
assert.equal(o7(conConvocati).valore, 100, "denominatore = convocati");
|
||||||
|
|
||||||
|
// Un non convocato che risponde "presente" non gonfia la percentuale.
|
||||||
|
const nonConvocato: ContestoObiettivi = {
|
||||||
|
...conConvocati,
|
||||||
|
presenze: { c1: { [g1!.id]: "presente", [g3!.id]: "presente" } },
|
||||||
|
};
|
||||||
|
assert.equal(o7(nonConvocato).valore, 50, "i non convocati non contano (1 su 2)");
|
||||||
|
|
||||||
|
// Arrotondamento: 179 presenze su 200 posti = 89,5% -> 90; 178 su 200 = 89%.
|
||||||
|
const rosa100 = Array.from({ length: 100 }, (_, i) => ({ ...g1!, id: `r${i}` }));
|
||||||
|
const rispondi = (presenti: number) =>
|
||||||
|
Object.fromEntries(rosa100.map((g, i) => [g.id, i < presenti ? "presente" : "assente"]));
|
||||||
|
const duePartite = (a: number, b: number): ContestoObiettivi => ({
|
||||||
|
eventi: [evento("t1", "2026-07-01", "allenamento"), evento("t2", "2026-07-02", "allenamento")],
|
||||||
|
presenze: {
|
||||||
|
t1: rispondi(a) as Record<string, "presente" | "assente">,
|
||||||
|
t2: rispondi(b) as Record<string, "presente" | "assente">,
|
||||||
|
},
|
||||||
|
pagelle: [],
|
||||||
|
});
|
||||||
|
assert.equal(o7(duePartite(90, 89), rosa100).valore, 90, "89,5% arrotonda a 90");
|
||||||
|
assert.equal(o7(duePartite(90, 88), rosa100).valore, 89, "89% resta 89");
|
||||||
|
assert.equal(o7(duePartite(100, 100), rosa100).valore, 100);
|
||||||
|
|
||||||
|
// Evento privo di risposte: pesa nel denominatore ma non nel numeratore.
|
||||||
|
const senzaRisposte: ContestoObiettivi = {
|
||||||
|
eventi: [evento("n1", "2026-07-01", "allenamento")],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(o7(senzaRisposte).valore, 0, "nessuna risposta: 0%");
|
||||||
|
|
||||||
|
// Convocati che non sono in rosa: non entrano nei posti.
|
||||||
|
const fuoriRosa: ContestoObiettivi = {
|
||||||
|
eventi: [{ ...evento("f1", "2026-07-01", "allenamento"), convocati: ["sconosciuto"] }],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(o7(fuoriRosa).valore, 0, "nessun posto valido: 0, non NaN");
|
||||||
|
|
||||||
|
// Eventi sociali e compleanni non fanno parte del denominatore.
|
||||||
|
const sociali: ContestoObiettivi = {
|
||||||
|
...base,
|
||||||
|
eventi: [
|
||||||
|
...base.eventi,
|
||||||
|
evento("c3", "2026-07-10", "evento"),
|
||||||
|
evento("c4", "2026-07-11", "compleanno"),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert.equal(o7(sociali).valore, 50, "solo allenamenti e partite");
|
||||||
|
}
|
||||||
|
|
||||||
const pagelle: VotoPagella[] = [
|
const pagelle: VotoPagella[] = [
|
||||||
{ match_id: "m1", votante_id: "g1", votato_id: "g2", voto: 7 },
|
{ match_id: "m1", votante_id: "g1", votato_id: "g2", voto: 7 },
|
||||||
|
|||||||
Reference in new issue
Block a user