Corregge il dettaglio MVP in classifica: solo partite, non tutti gli eventi

Il sottotitolo "N partite giocate" per il criterio MVP usava totaliEventiGiocatore(),
che conta anche gli allenamenti: un numero fuorviante perché l'MVP si vota solo alle
partite del calendario interno (eventi_app), non a quelle del sito CSI. Aggiunge
contaPartiteGiocate() (presenze.ts) e Giocatore.partiteGiocate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 11:02:38 +02:00
co-authored by Claude Sonnet 5
parent 15f058f760
commit 8353761b6e
7 changed files with 60 additions and 9 deletions
+3 -1
View File
@@ -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
+5
View File
@@ -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
+3
View File
@@ -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,
+28 -3
View File
@@ -14,28 +14,53 @@ export type MappaPresenze = Record<string, Record<string, Stato>>;
export type MappaTempiRisposta = Record<string, Record<string, string>>;
/** 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,
+4 -2
View File
@@ -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":
+14
View File
@@ -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,
+3 -3
View File
@@ -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"),