La percentuale di presenze degli ultimi 30 giorni decide le convocazioni, ma stava dentro due useMemo — scritta due volte, una per giocatore e una per la rosa — e nessun test la toccava. Le tre regole che la determinano non sono ovvie: la finestra è di 30 giorni, il ritardo conta come presenza, e convocati vuoto significa tutta la rosa attiva. presenze-mese-core.ts contiene ora il calcolo puro, presenze-mese.ts solo il collegamento agli hook. Comportamento invariato, inclusa la differenza fra i due percorsi: per giocatore si filtrano i convocati, per la rosa l'elenco vuoto si espande agli attivi. Il test fissa le tre regole e i casi che le rompono: nessun evento nella finestra dà 0 e non NaN, chi non è convocato resta fuori dal denominatore, chi non ha mai risposto ci resta dentro. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
2.8 KiB
TypeScript
73 lines
2.8 KiB
TypeScript
import type { Evento } from "./eventi";
|
|
import type { MappaPresenze } from "./presenze";
|
|
|
|
/**
|
|
* Calcolo puro della percentuale di presenze dell'ultimo mese, separato dagli hook
|
|
* (`presenze-mese.ts`) per poterlo testare: le tre regole che decidono il numero —
|
|
* finestra di 30 giorni, il ritardo che conta come presenza, `convocati` vuoto uguale
|
|
* a tutta la rosa — stanno qui.
|
|
*/
|
|
|
|
export type StatistichePresenza = { presenti: number; totali: number; percentuale: number };
|
|
|
|
const NESSUNA: StatistichePresenza = { presenti: 0, totali: 0, percentuale: 0 };
|
|
|
|
/** Chi è arrivato in ritardo era comunque all'allenamento: conta come presente. */
|
|
const eraPresente = (stato: string | undefined) => stato === "presente" || stato === "ritardo";
|
|
|
|
/** Finestra di 30 giorni che si chiude oggi, in date ISO. */
|
|
export function finestraMese(oggi: Date = new Date()): { da: string; a: string } {
|
|
const inizio = new Date(oggi);
|
|
inizio.setDate(inizio.getDate() - 30);
|
|
return { da: inizio.toISOString().slice(0, 10), a: oggi.toISOString().slice(0, 10) };
|
|
}
|
|
|
|
/** Partite e allenamenti dentro la finestra: gli altri eventi non contano. */
|
|
function eventiDelMese(eventi: Evento[], oggi: Date): Evento[] {
|
|
const { da, a } = finestraMese(oggi);
|
|
return eventi.filter(
|
|
(e) => (e.tipo === "partita" || e.tipo === "allenamento") && e.data >= da && e.data <= a,
|
|
);
|
|
}
|
|
|
|
function conPercentuale(presenti: number, totali: number): StatistichePresenza {
|
|
return { presenti, totali, percentuale: totali ? Math.round((presenti / totali) * 100) : 0 };
|
|
}
|
|
|
|
/** Presenze dell'ultimo mese di un singolo giocatore. */
|
|
export function presenzeUltimoMese(
|
|
giocatoreId: string | undefined,
|
|
eventi: Evento[],
|
|
presenze: MappaPresenze,
|
|
oggi: Date = new Date(),
|
|
): StatistichePresenza {
|
|
if (!giocatoreId) return NESSUNA;
|
|
const rilevanti = eventiDelMese(eventi, oggi).filter(
|
|
(e) => e.convocati.length === 0 || e.convocati.includes(giocatoreId),
|
|
);
|
|
const presenti = rilevanti.filter((e) => eraPresente(presenze[e.id]?.[giocatoreId])).length;
|
|
return conPercentuale(presenti, rilevanti.length);
|
|
}
|
|
|
|
/**
|
|
* Presenze dell'ultimo mese di tutta la rosa, in una mappa per id. Un evento senza
|
|
* convocati vale per tutti gli attivi, uno con l'elenco solo per i convocati.
|
|
*/
|
|
export function presenzeUltimoMeseTutti(
|
|
eventi: Evento[],
|
|
presenze: MappaPresenze,
|
|
idRosa: string[],
|
|
oggi: Date = new Date(),
|
|
): Record<string, StatistichePresenza> {
|
|
const out: Record<string, StatistichePresenza> = {};
|
|
for (const e of eventiDelMese(eventi, oggi)) {
|
|
for (const id of e.convocati.length > 0 ? e.convocati : idRosa) {
|
|
const rec = (out[id] ??= { presenti: 0, totali: 0, percentuale: 0 });
|
|
rec.totali += 1;
|
|
if (eraPresente(presenze[e.id]?.[id])) rec.presenti += 1;
|
|
}
|
|
}
|
|
for (const [id, rec] of Object.entries(out)) out[id] = conPercentuale(rec.presenti, rec.totali);
|
|
return out;
|
|
}
|