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
+254
-119
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
|
||||
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
|
||||
|
||||
|
||||
@@ -37,22 +37,22 @@ corrente dinamico (vedi sotto).
|
||||
|
||||
## Obiettivi definiti
|
||||
|
||||
| id | Obiettivo | Calcolo | Target | Fonte |
|
||||
| ----- | ---------------------------------- | ----------------------------------------------------------------------------- | --------------------- | ----------------------------- |
|
||||
| `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` |
|
||||
| `o7` | 250 presenze complessive | somma presenze di tutta la rosa, stagione intera | 250 | aggregato da `useRosa()` |
|
||||
| `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` |
|
||||
| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` |
|
||||
| `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 la cui data e ora sono già passate (dinamico) | 1 | `eventi_app` |
|
||||
| id | Obiettivo | Calcolo | Target | Fonte |
|
||||
| ----- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------- |
|
||||
| `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` |
|
||||
| `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` |
|
||||
| `o13` | 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
||||
| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` |
|
||||
| `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 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
|
||||
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
|
||||
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.
|
||||
@@ -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
|
||||
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
|
||||
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.
|
||||
@@ -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,
|
||||
filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico
|
||||
(evento dentro/fuori mese), scadenza dinamica.
|
||||
- **o7** (Supabase locale): scrive eventi/presenze reali, calcola `contaPresenzeGiocatore()` (la
|
||||
stessa funzione pura usata da `useRosa()` in produzione) sui dati riletti, verifica la somma.
|
||||
- **o7** (Supabase locale): scrive un allenamento e una partita con convocati e presenze reali,
|
||||
li rilegge con `leggiEventi()`/`leggiPresenze()` e verifica la percentuale (il ritardo conta).
|
||||
- **o11** (Supabase locale): scrive tre allenamenti e presenze reali, calcola
|
||||
`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
|
||||
|
||||
+26
-6
@@ -80,6 +80,27 @@ function percentualePresenzeMese(ctx: ContestoObiettivi, rosaSize: number, mese:
|
||||
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) {
|
||||
const daRispondere = ctx.eventi.filter((e) => e.tipo !== "compleanno");
|
||||
if (daRispondere.length === 0 || rosaSize === 0) return 0;
|
||||
@@ -97,7 +118,6 @@ export function obiettiviSquadra(
|
||||
ctx: ContestoObiettivi = contestoVuoto,
|
||||
oggi: Date = new Date(),
|
||||
): 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 vittorie = ctx.vittorie ?? 0;
|
||||
const mese = meseCorrente(oggi);
|
||||
@@ -126,11 +146,11 @@ export function obiettiviSquadra(
|
||||
},
|
||||
{
|
||||
id: "o7",
|
||||
titolo: "250 presenze complessive",
|
||||
descrizione: "Somma delle presenze di tutta la rosa in stagione",
|
||||
valore: somma((g) => g.presenze),
|
||||
target: 250,
|
||||
unita: "presenze",
|
||||
titolo: "Presenze collettive",
|
||||
descrizione: "Presenze di tutta la rosa su allenamenti e partite definiti in stagione",
|
||||
valore: percentualePresenzeCollettive(ctx, rosa),
|
||||
target: 90,
|
||||
unita: "%",
|
||||
emoji: "🤝",
|
||||
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`.
|
||||
* 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
|
||||
* 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`).
|
||||
*
|
||||
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
|
||||
@@ -23,7 +23,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { giocatori } from "@/lib/crapp-data";
|
||||
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 { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
@@ -282,33 +282,51 @@ if (!locale) {
|
||||
"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 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", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
tipo: "evento",
|
||||
titolo: "Test obiettivi o6",
|
||||
data: `${MESE_TEST}-${giorno}`,
|
||||
ora: "20:30",
|
||||
}),
|
||||
});
|
||||
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 tutti = await leggiEventi();
|
||||
const o6Di = (ids: string[], oggi: Date) =>
|
||||
obiettiviSquadra(
|
||||
giocatori,
|
||||
{ eventi: tutti.filter((e) => ids.includes(e.id)), presenze: {}, pagelle: [] },
|
||||
oggi,
|
||||
).find((o) => o.id === "o6")!;
|
||||
|
||||
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,
|
||||
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,
|
||||
"lo stesso evento non conta più il mese successivo (si azzera)",
|
||||
);
|
||||
@@ -386,68 +404,69 @@ if (!locale) {
|
||||
},
|
||||
);
|
||||
|
||||
await prova("o7 somma presenze calcolate da eventi/risposte reali del database", async () => {
|
||||
// o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da
|
||||
// `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita
|
||||
// la stessa funzione pura sui dati appena scritti, per verificare l'intera catena
|
||||
// DB -> contaPresenzeGiocatore -> o7, non solo la somma finale.
|
||||
const allenamentoId = `${PREFISSO}-o7-allenamento`;
|
||||
const partitaId = `${PREFISSO}-o7-partita`;
|
||||
const OGGI_STR = "2099-01-20";
|
||||
await prova(
|
||||
"o7 percentuale di presenze collettive da eventi/risposte reali del database",
|
||||
async () => {
|
||||
// o7 è la % di presenze su allenamenti e partite definiti (passati e futuri), con i
|
||||
// convocati come denominatore. Si esercita l'intera catena DB -> leggiEventi/leggiPresenze -> o7.
|
||||
const allenamentoId = `${PREFISSO}-o7-allenamento`;
|
||||
const partitaId = `${PREFISSO}-o7-partita`;
|
||||
const [g1, g2, g3] = giocatori;
|
||||
|
||||
for (const [id, tipo, data] of [
|
||||
[allenamentoId, "allenamento", "2099-01-05"],
|
||||
[partitaId, "partita", "2099-01-08"],
|
||||
] as const) {
|
||||
const inserito = await rest("eventi_app", {
|
||||
for (const [id, tipo, data] of [
|
||||
[allenamentoId, "allenamento", "2099-01-05"],
|
||||
[partitaId, "partita", "2099-01-08"],
|
||||
] as const) {
|
||||
const inserito = await rest("eventi_app", {
|
||||
method: "POST",
|
||||
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()}`);
|
||||
}
|
||||
|
||||
// g1 presente a entrambi, g2 presente e in ritardo (il ritardo conta), g3 assente a
|
||||
// entrambi: 4 presenze su 6 posti (3 convocati x 2 eventi) = 67%.
|
||||
const righe = [
|
||||
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
|
||||
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
|
||||
{ evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
|
||||
{ evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
|
||||
{ evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
|
||||
{ evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
|
||||
];
|
||||
const inserite = await rest("risposte_presenze", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }),
|
||||
body: JSON.stringify(righe),
|
||||
});
|
||||
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
||||
}
|
||||
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
|
||||
|
||||
// g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze,
|
||||
// il ritardo conta). g3: assente a entrambi (0 presenze).
|
||||
const [g1, g2, g3] = giocatori;
|
||||
const righe = [
|
||||
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
|
||||
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
|
||||
{ evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
|
||||
{ evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
|
||||
{ evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
|
||||
{ evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
|
||||
];
|
||||
const inserite = await rest("risposte_presenze", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(righe),
|
||||
});
|
||||
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
|
||||
const eventiReali = (await leggiEventi()).filter(
|
||||
(e) => e.id === allenamentoId || e.id === partitaId,
|
||||
);
|
||||
const presenzeReali = {
|
||||
...(await leggiPresenze(allenamentoId)),
|
||||
...(await leggiPresenze(partitaId)),
|
||||
};
|
||||
|
||||
const eventiReali = (await leggiEventi()).filter(
|
||||
(e) => e.id === allenamentoId || e.id === partitaId,
|
||||
);
|
||||
const presenzeReali = {
|
||||
...(await leggiPresenze(allenamentoId)),
|
||||
...(await leggiPresenze(partitaId)),
|
||||
};
|
||||
const o7 = obiettiviSquadra([g1!, g2!, g3!], {
|
||||
eventi: eventiReali,
|
||||
presenze: presenzeReali,
|
||||
pagelle: [],
|
||||
}).find((o) => o.id === "o7")!;
|
||||
|
||||
const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({
|
||||
...g,
|
||||
presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR),
|
||||
}));
|
||||
|
||||
const o7 = obiettiviSquadra(rosaConPresenzeReali, {
|
||||
eventi: [],
|
||||
presenze: {},
|
||||
pagelle: [],
|
||||
}).find((o) => o.id === "o7")!;
|
||||
|
||||
assert.equal(
|
||||
o7.valore,
|
||||
4,
|
||||
"g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database",
|
||||
);
|
||||
});
|
||||
assert.equal(
|
||||
o7.valore,
|
||||
67,
|
||||
"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 () => {
|
||||
const matchId = `${PREFISSO}-o12-m1`;
|
||||
|
||||
+104
-16
@@ -274,6 +274,32 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
|
||||
0,
|
||||
"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.
|
||||
const misti: ContestoObiettivi = {
|
||||
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 --------------------------------------------------------
|
||||
const sommaPresenze = giocatori.reduce((s, g) => s + g.presenze, 0);
|
||||
assert.equal(trova(vuoti, "o7").valore, sommaPresenze);
|
||||
assert.equal(
|
||||
trova(obiettiviSquadra([], contestoVuoto), "o7").valore,
|
||||
0,
|
||||
"rosa vuota: nessuna presenza",
|
||||
);
|
||||
// --- o7: presenze collettive (% su allenamenti e partite definiti, passati e futuri) ---
|
||||
{
|
||||
const [g1, g2, g3, g4] = giocatori;
|
||||
const rosa4 = [g1!, g2!, g3!, g4!];
|
||||
const o7 = (c: ContestoObiettivi, r = rosa4) => trova(obiettiviSquadra(r, c, OGGI_AGOSTO), "o7");
|
||||
|
||||
// Valori noti e indipendenti dai dati reali della rosa: non solo la stessa formula
|
||||
// ricalcolata sugli stessi dati, ma una somma verificabile a mente (5 + 10 + 15 = 30).
|
||||
const rosaControllata = giocatori.slice(0, 3).map((g, i) => ({ ...g, presenze: [5, 10, 15][i]! }));
|
||||
assert.equal(
|
||||
trova(obiettiviSquadra(rosaControllata, contestoVuoto), "o7").valore,
|
||||
30,
|
||||
"somma di presenze note, indipendente dal roster reale",
|
||||
);
|
||||
assert.equal(o7(contestoVuoto).titolo, "Presenze collettive");
|
||||
assert.equal(o7(contestoVuoto).target, 90);
|
||||
assert.equal(o7(contestoVuoto).valore, 0, "nessun evento: 0, non NaN");
|
||||
assert.equal(o7({ ...contestoVuoto }, []).valore, 0, "rosa vuota: 0");
|
||||
|
||||
// Allenamento passato (3 presenti su 4) + partita futura (1 presente su 4): 4 su 8 = 50%.
|
||||
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[] = [
|
||||
{ match_id: "m1", votante_id: "g1", votato_id: "g2", voto: 7 },
|
||||
|
||||
Reference in new issue
Block a user