From 1e4ec9113e4ecc3d4b4a47e9d8dd26f3ce9ae3fa Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Wed, 11 Nov 2026 10:33:49 +0100 Subject: [PATCH] Squadra e Profilo: ace, attacchi, muri e punti dai + dello scout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squadra → Stats: punti, ace e muri di squadra sommano i + delle valutazioni scout di tutte le partite. Profilo → Season: al posto di «Turni palloni» compaiono Attacchi, Muri, Ace e Punti (totale) del giocatore. Le partite del metodo precedente contano con le azioni. DD-008 aggiornata: dati solo informativi, fuori da badge e classifiche. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 5 +++ docs/decisions/DD-008.md | 3 ++ docs/modules/profilo-giocatore.md | 8 +++++ docs/modules/squadra.md | 7 ++++ src/lib/scout-store.ts | 40 ++++++++++++++++++++++ src/routes/profilo.tsx | 11 ++++-- src/routes/squadra.tsx | 4 +-- test/unit/scout-store.test.ts | 56 +++++++++++++++++++++++++++++++ 8 files changed, 130 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4c0f184..295b485 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,11 @@ versioni `0.y.z` erano pre-release. ### Modificato +- Squadra → Stats: «Punti squadra», «Ace squadra» e «Muri squadra» sommano i + dello scout di + tutte le partite (ace = + di battuta, muri = + di muro, punti = + di attacco, battuta e muro); + le partite del metodo precedente contano con le loro azioni. +- Profilo → Season: al posto di «Turni palloni» compaiono Ace, Attacchi, Muri e Punti (totale) + del giocatore, somma dei + dello scout di tutte le partite (DD-008: solo informativi). - Layout su schermi stretti (360-402 px, iPhone e Android): niente più pagina tagliata in larghezza. La tabella per giocatore dello scout sta nello schermo (il nome si accorcia), le griglie di riquadri non sfondano con nomi ed etichette lunghe, la pagina non scorre in diff --git a/docs/decisions/DD-008.md b/docs/decisions/DD-008.md index 6370795..2ffbe48 100644 --- a/docs/decisions/DD-008.md +++ b/docs/decisions/DD-008.md @@ -17,6 +17,9 @@ Le statistiche **personali** in profilo e squadra devono essere **eque per tutti - Badge e obiettivi usano presenze, MVP, pagelle, serie, cacche — metriche accessibili a tutti. - Lo scout resta strumento tecnico, non gioco. +- Aggiornamento 2026-11-11: in Profilo → Season compaiono, come semplice informazione personale, + i totali di ace, attacchi, muri e punti del giocatore ricavati dai + dello scout. Non entrano + in badge, obiettivi o classifiche: la regola sulla leva competitiva resta valida. **Riesame** Se la squadra chiede esplicitamente classifiche tecniche per ruolo. diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index b9256c2..ba34016 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -285,8 +285,16 @@ Sezione già presente. Contiene. - Voto medio - MVP - Serie +- **Ace, Attacchi, Muri e Punti (totale)**: somma, su tutte le partite scoutate, dei soli **+** + assegnati a quel giocatore nello scout live (ace = + di battuta, attacchi = + di attacco, + muri = + di muro; punti = ace + attacchi + muri, la ricezione non conta). Le partite scoutate + col metodo precedente, senza valutazioni, contano con le azioni registrate; se una partita ha + entrambe, valgono solo le valutazioni (`totaliTecnici()` in `scout-store.ts`). Il «Turni + palloni» non compare più qui: il turno resta nel banner palloni e nel badge dedicato. - Altre statistiche disponibili +Sono numeri informativi: non alimentano badge, obiettivi né classifiche (DD-008). + ### Badge Sezione già presente. diff --git a/docs/modules/squadra.md b/docs/modules/squadra.md index ffbb90f..ce86838 100644 --- a/docs/modules/squadra.md +++ b/docs/modules/squadra.md @@ -82,6 +82,13 @@ successivo non salta — 1, 1, 2, non 1, 1, 3); `dettaglioClassifica()` sceglie sottostatistica mostrare sotto il nome, coerente col criterio selezionato (es. "voti pagella" per il criterio media voto, non sempre "presenze consecutive"). +In cima alla tab, i riquadri «Punti squadra», «Ace squadra» e «Muri squadra» sommano **tutte le +partite scoutate** (`totaliSquadraValutazioni()`, che usa `totaliTecnici()`, in `scout-store.ts`), contando solo le valutazioni +**+** dello scout live: «Ace» = + di battuta, «Muri» = + di muro, «Punti» = + di attacco, battuta e +muro (la ricezione non è un punto messo a terra, come nel riquadro del giocatore). Le partite +scoutate col metodo precedente, senza valutazioni, contano con le loro azioni registrate (punti, +ace, muri); se una partita ha entrambe, valgono solo le valutazioni, così non si conta due volte. + Le altre tab di `/squadra` (Rosa, Obiettivi, Badge) sono viste diverse sugli stessi dati di `useRosa()`/`useObiettivi()`/`badges.ts`: non introducono altra logica di dominio, solo presentazione — le rispettive specifiche stanno in `badge.md` e `obiettivi-squadra.md`. diff --git a/src/lib/scout-store.ts b/src/lib/scout-store.ts index 7b2f310..01efaae 100644 --- a/src/lib/scout-store.ts +++ b/src/lib/scout-store.ts @@ -291,6 +291,46 @@ export function totaliSquadra(matches: ScoutMatch[]) { return out; } +export type TotaliTecnici = { punti: number; ace: number; attacchi: number; muri: number }; + +/** + * Somma, su tutte le partite scoutate, delle sole valutazioni **+** (battuta → ace, attacco → + * attacchi, muro → muri; punti = ace + attacchi + muri, non la ricezione). Con `giocatoreId` + * conta solo quel giocatore, altrimenti tutta la squadra. Una partita senza valutazioni (metodo + * precedente) conta con le sue azioni; se ha entrambe valgono solo le valutazioni, per non + * contare due volte. + */ +export function totaliTecnici(matches: ScoutMatch[], giocatoreId?: string): TotaliTecnici { + const out = { punti: 0, ace: 0, attacchi: 0, muri: 0 }; + for (const m of matches) { + if (m.valutazioni.length === 0) { + for (const [id, t] of totaliPerGiocatore(m.azioni)) { + if (giocatoreId !== undefined && id !== giocatoreId) continue; + out.ace += t.ace; + out.muri += t.muri; + out.attacchi += t.punti - t.ace - t.muri; + out.punti += t.punti; + } + continue; + } + for (const v of m.valutazioni) { + if (v.segno !== "+" || v.fondamentale === "ricezione") continue; + if (giocatoreId !== undefined && v.giocatoreId !== giocatoreId) continue; + if (v.fondamentale === "battuta") out.ace += 1; + if (v.fondamentale === "attacco") out.attacchi += 1; + if (v.fondamentale === "muro") out.muri += 1; + out.punti += 1; + } + } + return out; +} + +/** Punti, ace e muri di squadra per la tab Stats di `/squadra` (vedi `totaliTecnici`). */ +export function totaliSquadraValutazioni(matches: ScoutMatch[]) { + const { punti, ace, muri } = totaliTecnici(matches); + return { punti, ace, muri }; +} + /** * MVP ricavati dalle partite scoutate (mappa matchId → nome vincitore). * Le presenze personali restano su `risposte_presenze`, non sullo scout. diff --git a/src/routes/profilo.tsx b/src/routes/profilo.tsx index a6af7e3..2b94190 100644 --- a/src/routes/profilo.tsx +++ b/src/routes/profilo.tsx @@ -33,6 +33,7 @@ import { type GiocatoreSquadra, } from "@/lib/giocatori-squadra"; import { esci } from "@/lib/auth"; +import { totaliTecnici, useScoutMatches } from "@/lib/scout-store"; import { useIsAdmin } from "@/lib/ruoli"; import { useEmailNotifiche } from "@/lib/preferenze-utente"; import { Reveal } from "@/components/motion/Reveal"; @@ -80,9 +81,12 @@ function ProfiloGiocatore() { const votiSocial = useVotiSocial(); const g = useIo(); const ultimoMese = usePresenzeUltimoMese(g?.id); + const scoutMatches = useScoutMatches(); if (!g) return null; + const tecnici = totaliTecnici(scoutMatches, g.id); + const percPresenze = g.totaliEventi ? Math.round((g.presenze / g.totaliEventi) * 100) : 0; return ( @@ -122,9 +126,12 @@ function ProfiloGiocatore() { } label="Presenze di fila" /> - + + + + - + diff --git a/src/routes/squadra.tsx b/src/routes/squadra.tsx index 755af2b..472ed07 100644 --- a/src/routes/squadra.tsx +++ b/src/routes/squadra.tsx @@ -23,7 +23,7 @@ import { useGiocatoriSquadra, } from "@/lib/giocatori-squadra"; import { useSonoAllenatore } from "@/lib/user-store"; -import { totaliSquadra, useScoutMatches } from "@/lib/scout-store"; +import { totaliSquadraValutazioni, useScoutMatches } from "@/lib/scout-store"; import { useCsi } from "@/lib/csi"; import { partiteGiocate } from "@/lib/csi-core"; import { mediaSquadra, usePagelle } from "@/lib/pagelle"; @@ -110,7 +110,7 @@ function Squadra() { const [criterio, setCriterio] = useState("presenze"); const [filtroAperto, setFiltroAperto] = useState(false); const obiettivi = useObiettivi(); - const team = totaliSquadra(scoutMatches); + const team = totaliSquadraValutazioni(scoutMatches); const mediaPresenze = rosa.length ? Math.round( (rosa.reduce((s, g) => s + g.presenze / (g.totaliEventi || 1), 0) / rosa.length) * 100, diff --git a/test/unit/scout-store.test.ts b/test/unit/scout-store.test.ts index 4e29900..316426c 100644 --- a/test/unit/scout-store.test.ts +++ b/test/unit/scout-store.test.ts @@ -12,6 +12,8 @@ import { giocatoriConScout, totaliPerGiocatore, totaliSquadra, + totaliSquadraValutazioni, + totaliTecnici, type Azione, type Valutazione, type ScoutMatch, @@ -67,6 +69,60 @@ assert.deepEqual(totaliSquadra([match("m1", 3, 0, azioni), match("m2", 3, 1, azi }); assert.deepEqual(totaliSquadra([]), { punti: 0, ace: 0, muri: 0, errori: 0 }); +// --- totaliSquadraValutazioni: solo i + dello scout, fallback sulle azioni ------ +const vl = (fondamentale: Valutazione["fondamentale"], segno: "+" | "-", giocatoreId = "g1") => + ({ + id: Math.random().toString(), + giocatoreId, + fondamentale, + segno, + set: 1, + ts: 0, + }) as Valutazione; +const conVal = { + ...match("v1", 3, 0), + valutazioni: [ + vl("battuta", "+"), + vl("battuta", "+", "g2"), + vl("battuta", "-"), + vl("muro", "+"), + vl("muro", "-"), + vl("attacco", "+"), + vl("attacco", "+", "g2"), + vl("attacco", "-"), + vl("ricezione", "+"), + vl("ricezione", "+"), + ], +}; +assert.deepEqual(totaliSquadraValutazioni([conVal]), { punti: 5, ace: 2, muri: 1 }); +assert.deepEqual( + totaliSquadraValutazioni([conVal, conVal]), + { punti: 10, ace: 4, muri: 2 }, + "somma su più partite", +); +assert.deepEqual( + totaliSquadraValutazioni([match("a1", 3, 0, azioni)]), + { punti: 4, ace: 1, muri: 1 }, + "senza valutazioni: contano le azioni del metodo precedente", +); +assert.deepEqual( + totaliSquadraValutazioni([{ ...conVal, azioni }]), + { punti: 5, ace: 2, muri: 1 }, + "con entrambe valgono solo le valutazioni", +); +assert.deepEqual(totaliSquadraValutazioni([]), { punti: 0, ace: 0, muri: 0 }); + +// --- totaliTecnici: per giocatore, con attacchi ------------------------------- +assert.deepEqual(totaliTecnici([conVal], "g1"), { punti: 3, ace: 1, attacchi: 1, muri: 1 }); +assert.deepEqual(totaliTecnici([conVal], "g2"), { punti: 2, ace: 1, attacchi: 1, muri: 0 }); +assert.deepEqual(totaliTecnici([conVal], "assente"), { punti: 0, ace: 0, attacchi: 0, muri: 0 }); +assert.deepEqual(totaliTecnici([conVal, conVal]), { punti: 10, ace: 4, attacchi: 4, muri: 2 }); +assert.deepEqual( + totaliTecnici([match("a1", 3, 0, azioni)], "g1"), + { punti: 3, ace: 1, attacchi: 1, muri: 1 }, + "metodo precedente: ace, muro e attacco dalle azioni", +); + // --- giocatoriConScout: solo MVP da scout, presenze separate ----------------- const base = giocatori.find((g) => g.id === "g1")!; const conScout = giocatoriConScout([match("m1", 3, 0, azioni)], { m1: base.nome });