diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 266543b..47e04d1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -101,7 +101,9 @@ Prima versione, pre-release. scollegato dal criterio scelto: ora segue il criterio selezionato (`dettaglioClassifica` in `src/lib/rosa.ts`). Per Palloni mostra le volte consecutive in cui il giocatore li ha portati (nuovo campo `Giocatore.seriePalloni`, calcolato da `serieConsecutivaPalloni` in - `src/lib/palloni-core.ts`), non più le presenze. + `src/lib/palloni-core.ts`), non più le presenze. Per MVP mostra le partite giocate — solo + partite, non più allenamenti compresi (nuovo campo `Giocatore.partiteGiocate`, da + `contaPartiteGiocate()` in `src/lib/presenze.ts`). ### Sicurezza diff --git a/docs/modules/presenze.md b/docs/modules/presenze.md index a57b719..faf0c29 100644 --- a/docs/modules/presenze.md +++ b/docs/modules/presenze.md @@ -55,6 +55,11 @@ usePresenzeUltimoMese() → src/lib/presenze-mese.ts contaPresenzeGiocatore() alimenta il campo `presenze` del `Giocatore` in `useRosa()`, mostrato come StatTile nel profilo e in home (sezione «Colpo d'occhio», `index.tsx`). +contaPartiteGiocate() è contaPresenzeGiocatore() ristretto alle sole partite (non +allenamenti): alimenta `Giocatore.partiteGiocate`, usato nel sottotitolo della classifica +interna di Squadra quando si ordina per MVP — un conteggio di eventi generico (allenamenti +compresi) sarebbe fuorviante lì, perché l'MVP si vota solo alle partite. + --- sollecito (solo admin) --- Bottone "Sollecita" (RosaPresenze.tsx) → POST /api/public/sollecita-presenze ↓ diff --git a/src/lib/crapp-data.ts b/src/lib/crapp-data.ts index 87b598b..22591ef 100644 --- a/src/lib/crapp-data.ts +++ b/src/lib/crapp-data.ts @@ -28,6 +28,8 @@ export type Giocatore = { nascita: string; presenze: number; totaliEventi: number; + /** Solo partite (non allenamenti) a cui era presente o in ritardo. */ + partiteGiocate: number; streak: number; /** Serie consecutive per tipo: si azzerano in modo indipendente. */ serieAllenamenti: number; @@ -103,6 +105,7 @@ export const giocatori: Giocatore[] = rosaCSI iniziali: inizialiDa(r.nome), presenze: 0, totaliEventi: 0, + partiteGiocate: 0, streak: 0, serieAllenamenti: 0, seriePartite: 0, diff --git a/src/lib/presenze.ts b/src/lib/presenze.ts index 72b567f..a2f3cc7 100644 --- a/src/lib/presenze.ts +++ b/src/lib/presenze.ts @@ -14,28 +14,53 @@ export type MappaPresenze = Record>; export type MappaTempiRisposta = Record>; /** Allenamenti e partite CrAPP già passati, che contano per le statistiche di presenza. */ -function eventiContanoPresenze(eventi: Evento[], giocatoreId?: string, oggi = dataOggi()) { +function eventiContanoPresenze( + eventi: Evento[], + giocatoreId?: string, + oggi = dataOggi(), + tipo?: "partita" | "allenamento", +) { return eventi.filter( (e) => (e.tipo === "partita" || e.tipo === "allenamento") && + (tipo === undefined || e.tipo === tipo) && e.data < oggi && (giocatoreId === undefined || e.convocati.length === 0 || e.convocati.includes(giocatoreId)), ); } -/** Presenze effettive (presente o in ritardo) su eventi CrAPP. Senza eventi rilevanti restituisce 0. */ +/** + * Presenze effettive (presente o in ritardo) su eventi CrAPP. Senza eventi rilevanti + * restituisce 0. `tipo` filtra a un solo tipo di evento (es. solo partite); di default + * conta partite e allenamenti insieme, come il resto delle statistiche di presenza. + */ export function contaPresenzeGiocatore( giocatoreId: string, eventi: Evento[], presenze: MappaPresenze, oggi: string = dataOggi(), + tipo?: "partita" | "allenamento", ): number { - return eventiContanoPresenze(eventi, giocatoreId, oggi).filter((e) => { + return eventiContanoPresenze(eventi, giocatoreId, oggi, tipo).filter((e) => { const stato = presenze[e.id]?.[giocatoreId]; return stato === "presente" || stato === "ritardo"; }).length; } +/** + * Partite (non allenamenti) a cui il giocatore era presente o in ritardo: il dato giusto + * per contesti legati alle prestazioni in campo (es. MVP), a differenza di + * `totaliEventiGiocatore()` che è il denominatore delle presenze e include gli allenamenti. + */ +export function contaPartiteGiocate( + giocatoreId: string, + eventi: Evento[], + presenze: MappaPresenze, + oggi: string = dataOggi(), +): number { + return contaPresenzeGiocatore(giocatoreId, eventi, presenze, oggi, "partita"); +} + /** Eventi CrAPP rilevanti per il denominatore presenze di un giocatore. */ export function totaliEventiGiocatore( giocatoreId: string, diff --git a/src/lib/rosa.ts b/src/lib/rosa.ts index 671df11..6f184e4 100644 --- a/src/lib/rosa.ts +++ b/src/lib/rosa.ts @@ -10,6 +10,7 @@ import { useInfortuniERitardi } from "./infortuni"; import { useGiocatoreId } from "./user-store"; import { useEventi } from "./eventi"; import { + contaPartiteGiocate, contaPresenzeGiocatore, serieConferme, serieConsecutiva, @@ -88,6 +89,7 @@ export function useRosa(): Giocatore[] { iniziali: iniziali(g.nome, g.cognome), presenze: contaPresenzeGiocatore(g.id, eventi, mappaPresenze), totaliEventi: totaliEventiGiocatore(g.id, eventi), + partiteGiocate: contaPartiteGiocate(g.id, eventi, mappaPresenze), streak: serieConsecutiva(g.id, eventi, mappaPresenze), serieAllenamenti: serieConsecutiva(g.id, eventi, mappaPresenze, "allenamento"), seriePartite: serieConsecutiva(g.id, eventi, mappaPresenze, "partita"), @@ -128,7 +130,7 @@ export function dettaglioClassifica( g: { streak: number; votiPagella: number; - totaliEventi: number; + partiteGiocate: number; cacche: number; seriePalloni: number; }, @@ -138,7 +140,7 @@ export function dettaglioClassifica( case "mediaVoto": return `${g.votiPagella} voti pagella`; case "mvp": - return `${g.totaliEventi} partite giocate`; + return `${g.partiteGiocate} partite giocate`; case "cacchePartita": return `${g.cacche} giornate top`; case "palloni": diff --git a/test/unit/presenze.test.ts b/test/unit/presenze.test.ts index f8efc37..8405b94 100644 --- a/test/unit/presenze.test.ts +++ b/test/unit/presenze.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import type { Evento } from "@/lib/eventi"; import { conRisposta, + contaPartiteGiocate, contaPresenzeGiocatore, destinatariSollecito, serieConferme, @@ -91,6 +92,19 @@ assert.equal(totaliEventiGiocatore("g1", eventi, OGGI), 5, "gli eventi futuri no assert.equal(contaPresenzeGiocatore("g2", eventi, presenze, OGGI), 0, "chi non risponde è a zero"); assert.equal(totaliEventiGiocatore("g2", eventi, OGGI), 5, "il denominatore è uguale per tutti"); +// contaPartiteGiocate esclude gli allenamenti: su 5 eventi passati (4 allenamenti, 1 partita +// p1, presente), conta solo p1. +assert.equal( + contaPartiteGiocate("g1", eventi, presenze, OGGI), + 1, + "solo la partita p1: gli allenamenti non contano come partite giocate", +); +assert.equal( + contaPresenzeGiocatore("g1", eventi, presenze, OGGI, "allenamento"), + 3, + "contaPresenzeGiocatore con tipo esplicito filtra come contaPartiteGiocate ma per allenamenti", +); + // Solo partite e allenamenti: compleanni e altri eventi restano fuori. const conAltri: Evento[] = [ ...eventi, diff --git a/test/unit/rosa.test.ts b/test/unit/rosa.test.ts index 3f41ed2..ed128f4 100644 --- a/test/unit/rosa.test.ts +++ b/test/unit/rosa.test.ts @@ -28,7 +28,7 @@ assert.deepEqual(classificaRank([0, 0]), [1, 1], "parità anche a valore zero"); // --- dettaglioClassifica: il sottotitolo segue il criterio selezionato ------- -const g = { streak: 12, votiPagella: 18, totaliEventi: 22, cacche: 3, seriePalloni: 4 }; +const g = { streak: 12, votiPagella: 18, partiteGiocate: 15, cacche: 3, seriePalloni: 4 }; assert.equal( dettaglioClassifica(g, "presenze"), @@ -47,8 +47,8 @@ assert.equal( ); assert.equal( dettaglioClassifica(g, "mvp"), - "22 partite giocate", - "mvp: mostra le partite giocate, non le presenze consecutive", + "15 partite giocate", + "mvp: mostra le partite giocate (non gli eventi totali, che includerebbero gli allenamenti)", ); assert.equal( dettaglioClassifica(g, "cacchePartita"),