Corregge la classifica interna di Squadra a parità di punteggio
I giocatori a pari valore condividono ora la stessa posizione (dense rank) invece di essere numerati in sequenza, e il sottotitolo di ogni riga segue il criterio scelto invece di mostrare sempre le presenze consecutive. Per Palloni mostra le volte consecutive in cui il giocatore li ha portati (nuovo Giocatore.seriePalloni). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,19 @@ Prima versione, pre-release.
|
||||
(migration `m16_funzione_bonifica_dati_evento_orfani`, riservata al service role), coperta
|
||||
da test di integrazione invece che verificata solo a mano (DD-029).
|
||||
|
||||
### Corretto
|
||||
|
||||
- Classifica interna di Squadra: a parità di valore (es. stesse presenze) i giocatori
|
||||
condividono ora la stessa posizione invece di essere numerati in sequenza (`classificaRank`
|
||||
in `src/lib/rosa.ts`); la corona di primo posto va a tutti i pari merito in testa, non solo
|
||||
al primo dell'elenco.
|
||||
- Il sottotitolo di ogni riga della classifica interna di Squadra mostrava sempre le
|
||||
"presenze consecutive" anche ordinando per Media voto, MVP, Palloni o Cacche, un dato
|
||||
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.
|
||||
|
||||
### Sicurezza
|
||||
|
||||
- Migration `m4_solo_autenticati`: tolto al ruolo `anon` l'accesso alle tabelle dell'app
|
||||
|
||||
@@ -39,6 +39,11 @@ compaiono.
|
||||
`TurnoPalloni.tsx`/`PromemoriaPalloni.tsx`). Conta solo gli eventi già passati (`e.data <
|
||||
oggi`, stesso criterio delle presenze): un turno assegnato in anticipo per un allenamento
|
||||
futuro non è ancora "portato", quindi non sale finché quel giorno non arriva.
|
||||
- `serieConsecutivaPalloni()` (`palloni-core.ts`) calcola le volte **consecutive** in cui il
|
||||
giocatore ha portato i palloni (`Giocatore.seriePalloni` in `rosa.ts`), mostrate nel
|
||||
sottotitolo della classifica interna di Squadra quando si ordina per Palloni. Stesso
|
||||
criterio "solo eventi già passati" di `conteggioTurni()`; un evento passato senza turno
|
||||
confermato non spezza la serie di nessuno (viene saltato, non conta come "non portati").
|
||||
- `TurnoPalloni.tsx` mostra/assegna il turno sulla card di un evento; `PromemoriaPalloni.tsx`
|
||||
è il banner in Home per il giocatore di turno.
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ export type Giocatore = {
|
||||
votiPagella: number;
|
||||
/** Quante volte ha portato i palloni. */
|
||||
palloni: number;
|
||||
/** Volte consecutive (fino a oggi) in cui ha portato i palloni. */
|
||||
seriePalloni: number;
|
||||
/** Partite di campionato con almeno 3 cacche dichiarate. */
|
||||
cacche: number;
|
||||
/** Media di cacche dichiarate per partita. */
|
||||
@@ -111,6 +113,7 @@ export const giocatori: Giocatore[] = rosaCSI
|
||||
infortuni: 0,
|
||||
ritardi: 0,
|
||||
palloni: 0,
|
||||
seriePalloni: 0,
|
||||
cacche: 0,
|
||||
cacchePartita: 0,
|
||||
}))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { formatData } from "./crapp-data";
|
||||
import type { Evento } from "./eventi";
|
||||
import { dataOggi } from "./scout-live";
|
||||
import { aggiornaSerie } from "./serie";
|
||||
|
||||
export type Turno = { evento_id: string; giocatore_id: string; aggiornato_da: string | null };
|
||||
|
||||
@@ -81,6 +82,23 @@ export function conteggioTurni(
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Volte consecutive in cui il giocatore ha portato i palloni, contando solo eventi già
|
||||
* passati (stesso criterio `e.data < oggi` di `conteggioTurni`): un evento passato senza
|
||||
* turno confermato non spezza la serie di nessuno, perché non dice ancora chi ha portato
|
||||
* i palloni quella volta.
|
||||
*/
|
||||
export function serieConsecutivaPalloni(
|
||||
giocatoreId: string,
|
||||
turni: Record<string, string>,
|
||||
eventi: Evento[],
|
||||
oggi: string = dataOggi(),
|
||||
): number {
|
||||
return eventiPalloni(eventi)
|
||||
.filter((e) => e.data < oggi && turni[e.id])
|
||||
.reduce((serie, e) => aggiornaSerie(serie, turni[e.id] === giocatoreId), 0);
|
||||
}
|
||||
|
||||
export function eventiDelGiorno(eventi: Evento[], isoData: string): Evento[] {
|
||||
return eventiPalloni(eventi).filter((e) => e.data === isoData);
|
||||
}
|
||||
|
||||
+51
-1
@@ -4,7 +4,7 @@ import { nomeCompleto, useGiocatoriSquadra } from "./giocatori-squadra";
|
||||
import { mvpVintiPerGiocatore, useVotiMvp } from "./mvp-voti";
|
||||
import { mediePagelle, usePagelle } from "./pagelle";
|
||||
import { statisticheCacche, useCacche } from "./cacche";
|
||||
import { conteggioTurni } from "./palloni-core";
|
||||
import { conteggioTurni, serieConsecutivaPalloni } from "./palloni-core";
|
||||
import { useTurniPalloni } from "./palloni";
|
||||
import { useInfortuniERitardi } from "./infortuni";
|
||||
import { useGiocatoreId } from "./user-store";
|
||||
@@ -96,6 +96,7 @@ export function useRosa(): Giocatore[] {
|
||||
mediaVoto: medie[g.id]?.media ?? 0,
|
||||
votiPagella: medie[g.id]?.voti ?? 0,
|
||||
palloni: palloni[g.id] ?? 0,
|
||||
seriePalloni: serieConsecutivaPalloni(g.id, turniSalvati, eventi),
|
||||
cacche: statCacche[g.id]?.giornateTop ?? 0,
|
||||
cacchePartita: statCacche[g.id]?.media ?? 0,
|
||||
infortuni: infortuni[g.id] ?? 0,
|
||||
@@ -115,6 +116,55 @@ export function useRosa(): Giocatore[] {
|
||||
]);
|
||||
}
|
||||
|
||||
/** Criteri di ordinamento della classifica interna di Squadra. */
|
||||
export type CriterioClassifica = "presenze" | "mediaVoto" | "mvp" | "palloni" | "cacchePartita";
|
||||
|
||||
/**
|
||||
* Dettaglio mostrato sotto il nome nella classifica interna, coerente col criterio
|
||||
* selezionato: mostrare sempre le "presenze consecutive" aveva senso solo per Presenze,
|
||||
* per gli altri criteri era un dato fuorviante perché scollegato dal valore in classifica.
|
||||
*/
|
||||
export function dettaglioClassifica(
|
||||
g: {
|
||||
streak: number;
|
||||
votiPagella: number;
|
||||
totaliEventi: number;
|
||||
cacche: number;
|
||||
seriePalloni: number;
|
||||
},
|
||||
criterio: CriterioClassifica,
|
||||
): string {
|
||||
switch (criterio) {
|
||||
case "mediaVoto":
|
||||
return `${g.votiPagella} voti pagella`;
|
||||
case "mvp":
|
||||
return `${g.totaliEventi} partite giocate`;
|
||||
case "cacchePartita":
|
||||
return `${g.cacche} giornate top`;
|
||||
case "palloni":
|
||||
return `${g.seriePalloni} volte consecutive`;
|
||||
default:
|
||||
return `${g.streak} presenze consecutive`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Posizione in classifica ("dense rank"): a parità di valore i giocatori condividono la
|
||||
* stessa posizione e il numero successivo non salta (1, 1, 2 — non 1, 1, 3). `valori` deve
|
||||
* essere già ordinato in modo decrescente, coerente con l'ordine visualizzato.
|
||||
*/
|
||||
export function classificaRank(valori: number[]): number[] {
|
||||
const rank: number[] = [];
|
||||
for (let i = 0; i < valori.length; i++) {
|
||||
if (i > 0 && valori[i] === valori[i - 1]) {
|
||||
rank.push(rank[i - 1] ?? 1);
|
||||
} else {
|
||||
rank.push((rank[i - 1] ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
}
|
||||
|
||||
/** Il giocatore selezionato sul dispositivo, con le statistiche complete. */
|
||||
export function useIo(): Giocatore | null {
|
||||
const id = useGiocatoreId();
|
||||
|
||||
+13
-6
@@ -7,7 +7,13 @@ import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
||||
import { Avatar } from "@/components/crapp/Avatar";
|
||||
import { formatData } from "@/lib/crapp-data";
|
||||
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
||||
import { useRosa, useObiettivi } from "@/lib/rosa";
|
||||
import {
|
||||
useRosa,
|
||||
useObiettivi,
|
||||
classificaRank,
|
||||
dettaglioClassifica,
|
||||
type CriterioClassifica,
|
||||
} from "@/lib/rosa";
|
||||
import { usePresenzeUltimoMeseTutti } from "@/lib/presenze-mese";
|
||||
import { totaliSquadra, useScoutMatches } from "@/lib/scout-store";
|
||||
import { useCsi } from "@/lib/csi";
|
||||
@@ -70,7 +76,7 @@ const criteri = [
|
||||
{ id: "cacchePartita", label: "Cacche" },
|
||||
] as const;
|
||||
|
||||
type Criterio = (typeof criteri)[number]["id"];
|
||||
type Criterio = CriterioClassifica;
|
||||
|
||||
function valore(
|
||||
g: { presenze: number; mediaVoto: number; mvp: number; palloni: number; cacchePartita: number },
|
||||
@@ -95,6 +101,7 @@ function Squadra() {
|
||||
)
|
||||
: 0;
|
||||
const ordinati = [...rosa].sort((a, b) => valore(b, criterio) - valore(a, criterio));
|
||||
const rank = classificaRank(ordinati.map((g) => valore(g, criterio)));
|
||||
const max = ordinati[0] ? valore(ordinati[0], criterio) || 1 : 1;
|
||||
const { data: csi } = useCsi();
|
||||
const matchGiocati = csi ? partiteGiocate(csi.partite).length : scoutMatches.length;
|
||||
@@ -288,17 +295,17 @@ function Squadra() {
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-secondary font-display text-lg">
|
||||
{i + 1}
|
||||
{rank[i]}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-bold">
|
||||
{g.nome}
|
||||
{i === 0 ? (
|
||||
{rank[i] === 1 ? (
|
||||
<Crown className="ml-1 inline h-3.5 w-3.5 text-warning" />
|
||||
) : null}
|
||||
</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
#{g.numero} · {g.ruolo} · {g.streak} presenze consecutive
|
||||
#{g.numero} · {g.ruolo} · {dettaglioClassifica(g, criterio)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,7 +324,7 @@ function Squadra() {
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[mediaPresenze, matchGiocati, pagelle, team, filtroAperto, criterio, ordinati, max],
|
||||
[mediaPresenze, matchGiocati, pagelle, team, filtroAperto, criterio, ordinati, rank, max],
|
||||
);
|
||||
|
||||
const contenutoObiettivi = useMemo(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
eventoPrecedente,
|
||||
eventoSuccessivo,
|
||||
oggiISO,
|
||||
serieConsecutivaPalloni,
|
||||
} from "@/lib/palloni-core";
|
||||
|
||||
const rosa = giocatori.map((g) => ({ id: g.id, nome: g.nome }));
|
||||
@@ -213,4 +214,55 @@ assert.deepEqual(
|
||||
"senza turni assegnati non c'è nessuno da avvisare",
|
||||
);
|
||||
|
||||
// --- serieConsecutivaPalloni: volte consecutive in cui li ha portati lui -----
|
||||
const eventiSerie: Evento[] = [
|
||||
evento("s1", "2026-04-01", "partita"),
|
||||
evento("s2", "2026-04-02", "partita"),
|
||||
evento("s3", "2026-04-03", "partita"),
|
||||
evento("s4", "2026-04-04", "partita"),
|
||||
evento("s5", "2026-04-05", "partita"), // futuro rispetto a OGGI_SERIE
|
||||
];
|
||||
const OGGI_SERIE = "2026-04-05";
|
||||
|
||||
assert.equal(
|
||||
serieConsecutivaPalloni(
|
||||
"g1",
|
||||
{ s1: "g1", s2: "g1", s3: "g2", s4: "g1" },
|
||||
eventiSerie,
|
||||
OGGI_SERIE,
|
||||
),
|
||||
1,
|
||||
"un turno di un altro giocatore azzera la serie: conta solo l'ultimo tratto consecutivo",
|
||||
);
|
||||
assert.equal(
|
||||
serieConsecutivaPalloni(
|
||||
"g2",
|
||||
{ s1: "g1", s2: "g1", s3: "g2", s4: "g1" },
|
||||
eventiSerie,
|
||||
OGGI_SERIE,
|
||||
),
|
||||
0,
|
||||
"l'ultimo evento passato non è suo: serie a zero anche se ne ha uno nel mezzo",
|
||||
);
|
||||
assert.equal(
|
||||
serieConsecutivaPalloni("g1", { s1: "g1", s2: "g1", s3: "g1" }, eventiSerie, OGGI_SERIE),
|
||||
3,
|
||||
"tre volte di fila fino all'ultimo evento passato",
|
||||
);
|
||||
assert.equal(
|
||||
serieConsecutivaPalloni("g1", { s1: "g1", s2: "g1", s4: "g1" }, eventiSerie, OGGI_SERIE),
|
||||
3,
|
||||
"s3 senza turno confermato non spezza la serie: viene saltato, non conta contro nessuno",
|
||||
);
|
||||
assert.equal(
|
||||
serieConsecutivaPalloni("g1", { s1: "g1", s5: "g1" }, eventiSerie, OGGI_SERIE),
|
||||
1,
|
||||
"il turno dell'evento futuro (s5) non è ancora contato",
|
||||
);
|
||||
assert.equal(
|
||||
serieConsecutivaPalloni("g1", {}, eventiSerie, OGGI_SERIE),
|
||||
0,
|
||||
"nessun turno assegnato: nessuna serie",
|
||||
);
|
||||
|
||||
console.log("palloni-core: ok");
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/** Check della classifica della rosa: `bun test/unit/rosa.test.ts`. */
|
||||
import assert from "node:assert/strict";
|
||||
import { classificaRank, dettaglioClassifica } from "@/lib/rosa";
|
||||
|
||||
// --- classificaRank: dense rank, a parità di valore stessa posizione -----------
|
||||
|
||||
assert.deepEqual(
|
||||
classificaRank([4, 4, 3]),
|
||||
[1, 1, 2],
|
||||
"due giocatori a pari merito condividono la posizione, il numero successivo non salta",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
classificaRank([5, 4, 3, 2]),
|
||||
[1, 2, 3, 4],
|
||||
"nessun pareggio: posizione progressiva normale",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
classificaRank([3, 3, 3, 1]),
|
||||
[1, 1, 1, 2],
|
||||
"tre giocatori a pari merito in testa condividono tutti la posizione #1",
|
||||
);
|
||||
|
||||
assert.deepEqual(classificaRank([]), [], "rosa vuota: nessuna posizione");
|
||||
|
||||
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 };
|
||||
|
||||
assert.equal(
|
||||
dettaglioClassifica(g, "presenze"),
|
||||
"12 presenze consecutive",
|
||||
"presenze: mostra la serie di presenze consecutive",
|
||||
);
|
||||
assert.equal(
|
||||
dettaglioClassifica(g, "palloni"),
|
||||
"4 volte consecutive",
|
||||
"palloni: mostra le volte consecutive in cui ha portato i palloni, non le presenze",
|
||||
);
|
||||
assert.equal(
|
||||
dettaglioClassifica(g, "mediaVoto"),
|
||||
"18 voti pagella",
|
||||
"media voto: mostra quanti voti compongono la media",
|
||||
);
|
||||
assert.equal(
|
||||
dettaglioClassifica(g, "mvp"),
|
||||
"22 partite giocate",
|
||||
"mvp: mostra le partite giocate, non le presenze consecutive",
|
||||
);
|
||||
assert.equal(
|
||||
dettaglioClassifica(g, "cacchePartita"),
|
||||
"3 giornate top",
|
||||
"cacche: mostra le giornate da primo classificato",
|
||||
);
|
||||
|
||||
console.log("rosa: ok");
|
||||
Reference in New Issue
Block a user