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 });