Applica una soglia minima di campione a Media voto e MVP in home (DD-028)
Rimuove l'hint statico "+2 questo mese" dalla StatTile Presenze. La StatTile Media voto usa ora la stessa soglia minima di voti del badge Pagellone (VOTI_MINIMI_PAGELLA), tramite la nuova funzione pura testabile mediaVotoColpoDOcchio(). L'MVP di partita richiede un quorum minimo di 2 voti totali (VOTI_MINIMI_MVP) oltre al margine netto già richiesto: un solo voto non assegna più la vittoria, con effetto anche sui conteggi già mostrati. Aggiorna test unitari e di integrazione, e documenta la decisione in DESIGN_DECISIONS.md, pagelle.md, mvp.md e CHANGELOG.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+21
-4
@@ -60,6 +60,9 @@ export function useVotaMvp() {
|
||||
|
||||
export type ConteggioMvp = { id: string; nome: string; voti: number };
|
||||
|
||||
/** Voti minimi in una partita perché l'MVP possa essere assegnato (un solo voto non decide). */
|
||||
export const VOTI_MINIMI_MVP = 2;
|
||||
|
||||
/** Conteggio voti di una partita, dal più votato. */
|
||||
export function conteggioPartita(voti: VotoMvp[], matchId: string): ConteggioMvp[] {
|
||||
const map = new Map<string, ConteggioMvp>();
|
||||
@@ -83,8 +86,13 @@ export function vincitoriMvp(voti: VotoMvp[]): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const [matchId] of perMatch) {
|
||||
const top = conteggioPartita(voti, matchId);
|
||||
// In caso di parità nessun MVP assegnato finché il voto non si sblocca.
|
||||
if (top.length > 0 && (top.length === 1 || top[0]!.voti > top[1]!.voti)) {
|
||||
const totaleVoti = top.reduce((s, c) => s + c.voti, 0);
|
||||
// In caso di parità, o sotto il quorum minimo, nessun MVP assegnato.
|
||||
if (
|
||||
totaleVoti >= VOTI_MINIMI_MVP &&
|
||||
top.length > 0 &&
|
||||
(top.length === 1 || top[0]!.voti > top[1]!.voti)
|
||||
) {
|
||||
out[matchId] = top[0]!.nome;
|
||||
}
|
||||
}
|
||||
@@ -95,13 +103,22 @@ export function mioVoto(voti: VotoMvp[], matchId: string, votanteId: string) {
|
||||
return voti.find((v) => v.match_id === matchId && v.votante_id === votanteId) ?? null;
|
||||
}
|
||||
|
||||
/** MVP vinti per giocatore, contando una vittoria per partita votata. */
|
||||
/**
|
||||
* MVP vinti per giocatore, contando una vittoria per partita votata (una partita in pareggio
|
||||
* al vertice, o sotto il quorum minimo di voti, non assegna vittorie a nessuno). Senza voti
|
||||
* restituisce una mappa vuota.
|
||||
*/
|
||||
export function mvpVintiPerGiocatore(voti: VotoMvp[]): Record<string, number> {
|
||||
const out: Record<string, number> = {};
|
||||
const matchIds = new Set(voti.map((v) => v.match_id));
|
||||
for (const matchId of matchIds) {
|
||||
const top = conteggioPartita(voti, matchId);
|
||||
if (top.length > 0 && (top.length === 1 || top[0]!.voti > top[1]!.voti)) {
|
||||
const totaleVoti = top.reduce((s, c) => s + c.voti, 0);
|
||||
if (
|
||||
totaleVoti >= VOTI_MINIMI_MVP &&
|
||||
top.length > 0 &&
|
||||
(top.length === 1 || top[0]!.voti > top[1]!.voti)
|
||||
) {
|
||||
const id = top[0]!.id;
|
||||
out[id] = (out[id] ?? 0) + 1;
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { VOTI_MINIMI_PAGELLA } from "./badges";
|
||||
|
||||
/** Voto anonimo da 1 a 10 dato a un compagno per una partita. */
|
||||
export type VotoPagella = {
|
||||
@@ -62,7 +63,8 @@ function arrotonda(n: number) {
|
||||
|
||||
/**
|
||||
* Media storica di ciascun giocatore su tutti i voti mai ricevuti (l'app non ha un concetto
|
||||
* di stagione/reset): giocatoreId -> media e numero di voti.
|
||||
* di stagione/reset): giocatoreId -> media e numero di voti. Chi non ha ancora ricevuto voti
|
||||
* non compare nella mappa (nessuna divisione per zero): sta al chiamante gestire il fallback.
|
||||
*/
|
||||
export function mediePagelle(voti: VotoPagella[]): Record<string, MediaPagella> {
|
||||
const somma: Record<string, { tot: number; n: number }> = {};
|
||||
@@ -93,6 +95,15 @@ export function mieiVoti(voti: VotoPagella[], matchId: string, votanteId: string
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Media voto da mostrare nella StatTile home (sezione «Colpo d'occhio», DD-028): sotto
|
||||
* `VOTI_MINIMI_PAGELLA` voti ricevuti, `—` invece della media grezza — stessa soglia del
|
||||
* badge Pagellone, non applicata invece dalle StatTile di profilo e squadra.
|
||||
*/
|
||||
export function mediaVotoColpoDOcchio(g: { mediaVoto: number; votiPagella: number }): number | "—" {
|
||||
return g.votiPagella >= VOTI_MINIMI_PAGELLA ? g.mediaVoto : "—";
|
||||
}
|
||||
|
||||
/** Media pagelle di tutta la squadra su tutte le partite. */
|
||||
export function mediaSquadra(voti: VotoPagella[]) {
|
||||
if (voti.length === 0) return 0;
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ function eventiContanoPresenze(eventi: Evento[], giocatoreId?: string, oggi = da
|
||||
);
|
||||
}
|
||||
|
||||
/** Presenze effettive (presente o in ritardo) su eventi CrAPP. */
|
||||
/** Presenze effettive (presente o in ritardo) su eventi CrAPP. Senza eventi rilevanti restituisce 0. */
|
||||
export function contaPresenzeGiocatore(
|
||||
giocatoreId: string,
|
||||
eventi: Evento[],
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useCsi } from "@/lib/csi";
|
||||
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||
import { useScoutMatches } from "@/lib/scout-store";
|
||||
import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti";
|
||||
import { mediaVotoColpoDOcchio } from "@/lib/pagelle";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
head: () => ({
|
||||
@@ -238,8 +239,8 @@ function Index() {
|
||||
|
||||
<Section titolo="Colpo d'occhio" indice={6}>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<StatTile valore={giocatore.presenze} label="Presenze" hint="+2 questo mese" />
|
||||
<StatTile valore={giocatore.mediaVoto || "—"} label="Media voto" />
|
||||
<StatTile valore={giocatore.presenze} label="Presenze" />
|
||||
<StatTile valore={mediaVotoColpoDOcchio(giocatore)} label="Media voto" />
|
||||
<StatTile valore={giocatore.mvp} label="MVP" />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
Reference in New Issue
Block a user