diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2d864b1..a069308 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,11 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +### Aggiunto + +- Pagina partita, sezione «Scout»: i risultati dello scout si leggono nell'app (percentuale di + + e conteggi per fondamentale, per partita o per set, e tabella per giocatore), non solo nel CSV. + ### Modificato - Sondaggio cacche: la classifica completa è visibile a tutti i giocatori, anche a sondaggio diff --git a/docs/modules/scout-live.md b/docs/modules/scout-live.md index c7d40e8..5beeb01 100644 --- a/docs/modules/scout-live.md +++ b/docs/modules/scout-live.md @@ -126,6 +126,17 @@ pagina partita, sezione "Scout". --- +## Lettura nell'app + +La sezione «Scout» della pagina partita (`ScoutReport.tsx`, visibile a chi vede la pagina; il CSV +resta per i soli admin) mostra, per la partita o per un singolo set (selettore in alto, se i set +sono più di uno): per ciascun fondamentale la percentuale di **+**, una barra verde/rossa e i +conteggi di squadra; poi la tabella per giocatore con **+ / −** di ogni fondamentale. I totali +vengono da `sintesiValutazioni()` (`scout-store.ts`). Le azioni (P/A/M/E) compaiono in un blocco a +parte solo se la partita ne ha, cioè quelle scoutate prima di DD-053. Il pulsante (icona i) apre una guida in linea su valutazioni, percentuale, tabella e set. + +--- + ## Sondaggio cacche `SondaggioCacche.tsx` chiede "quante cacche hai fatto prima di questa partita" (0-5+), sempre @@ -153,11 +164,8 @@ calcolati a runtime). - Lo stato in corso è raggiungibile solo il giorno della partita: se nessuno preme «Fine partita» entro la mezzanotte i dati restano in `scout_live` ma dall'app non si riprendono più. -- Le valutazioni dei set chiusi e delle partite finite sono salvate ma non si rivedono nell'app: lo - scout mostra solo il set in corso e l'unica lettura è il CSV admin. Evoluzione: tabella nella - pagina partita. -- Il blocco «Scout» della pagina partita mostra ancora Punti/Ace/Muri/Errori (a zero per le - partite di sole valutazioni) e non le valutazioni, che sono solo nel CSV admin. +- Lo scout in corso mostra solo il set in corso: le valutazioni dei set chiusi si rivedono solo + nella pagina partita, dopo «Fine partita». - Scout aperto a tutta la squadra: nessun filtro su chi può registrare le azioni, l'unica garanzia è il lock di sessione (vedi sopra). - Possibile, per quanto improbabile, doppio "successo" applicativo nel prendere il lock: @@ -171,6 +179,5 @@ calcolati a runtime). ## Evoluzioni possibili -- Mostrare le valutazioni per set e giocatore nella pagina della partita (oggi solo nel CSV admin) - e, nello scout, in sola lettura quelle dei set chiusi. +- Nello scout, in sola lettura, le valutazioni dei set chiusi. - Realtime (Supabase Realtime) per aggiornare la sessione condivisa senza refresh manuale. diff --git a/src/components/crapp/ScoutReport.tsx b/src/components/crapp/ScoutReport.tsx new file mode 100644 index 0000000..869d68b --- /dev/null +++ b/src/components/crapp/ScoutReport.tsx @@ -0,0 +1,290 @@ +import { useState } from "react"; +import { BarChart3, Download, Info } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Card } from "@/components/crapp/ui-bits"; +import type { Giocatore } from "@/lib/crapp-data"; +import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export"; +import { + fondamentali, + sintesiValutazioni, + totaliPerGiocatore, + totaliSquadra, + type ScoutMatch, +} from "@/lib/scout-store"; + +const pct = (piu: number, meno: number) => + piu + meno === 0 ? null : Math.round((piu / (piu + meno)) * 100); + +/** Lettura dello scout di una partita: squadra per fondamentale, set per set, dettaglio per giocatore. */ +export function ScoutReport({ + scout, + rosa, + admin, +}: { + scout: ScoutMatch; + rosa: Giocatore[]; + admin: boolean; +}) { + // undefined = tutta la partita. + const [set, setSet] = useState(undefined); + const [guida, setGuida] = useState(false); + const sets = [...new Set(scout.valutazioni.map((v) => v.set))].sort((a, b) => a - b); + const { squadra, perGiocatore } = sintesiValutazioni(scout.valutazioni, set); + const righe = perGiocatore + .map((r) => ({ ...r, g: rosa.find((x) => x.id === r.giocatoreId) })) + .filter((r): r is typeof r & { g: Giocatore } => !!r.g) + .sort((a, b) => a.g.numero - b.g.numero); + + const azioni = totaliPerGiocatore(scout.azioni); + const totaliAzioni = totaliSquadra([scout]); + + return ( + +
+

+ Valutazione scout +

+ +
+ {guida ? ( +
+ {[ + [ + "Cosa misura", + <> + Durante la partita lo scout assegna a ciascun giocatore una valutazione{" "} + + (esecuzione positiva) o{" "} + − (esecuzione negativa) su quattro fondamentali: + battuta, ricezione, attacco e muro. Sono giudizi qualitativi, non punti: non + incidono su risultato, classifiche o badge. + , + ], + [ + "Indicatori di squadra", + <> + Ogni scheda riporta, per fondamentale, la percentuale di valutazioni positive (+ ÷ + (+ + −)), la barra di composizione verde/rossa e il numero di + e −. Un valore alto + indica un fondamentale eseguito con continuità. + , + ], + [ + "Dettaglio per giocatore", + <> + La tabella elenca, per ogni giocatore, i + e i − di ciascun fondamentale nel formato{" "} + + / −: «3 / 1» indica tre valutazioni positive e una negativa. Compaiono solo + i giocatori con almeno una valutazione nel periodo selezionato. + , + ], + [ + "Periodo", + <> + «Totale» aggrega l'intera partita; «Set 1», «Set 2»… isolano un singolo set e + permettono di osservare l'andamento durante la gara. + , + ], + [ + "Interpretazione", + <> + Il campione è ridotto: con pochi tocchi la percentuale è poco significativa (un solo + + su un solo tocco vale 100%). I dati riflettono il giudizio dello scout in servizio + e vanno letti come indicazione di squadra, non come voto individuale. + , + ], + [ + "Partite precedenti", + <> + Per le partite scoutate con il metodo precedente è mostrato anche il riepilogo delle + azioni registrate (punti, ace, muri, errori). + , + ], + ].map(([titolo, testo]) => ( +
+
+ {titolo} +
+
{testo}
+
+ ))} +
+ ) : null} + {scout.valutazioni.length > 0 ? ( + <> + {sets.length > 1 ? ( +
+ {[undefined, ...sets].map((s) => ( + + ))} +
+ ) : null} + +
+ {fondamentali.map((f) => { + const { piu, meno } = squadra[f.id]; + const p = pct(piu, meno); + return ( +
+
+

+ {f.label} +

+

+ {p === null ? "—" : `${p}%`} +

+
+
+ + +
+

+ +{piu} + · + −{meno} +

+
+ ); + })} +
+ +

+ Dettaglio giocatori +

+ {righe.length === 0 ? ( +

+ Nessuna valutazione in questo set. +

+ ) : ( +
+ + + + + {fondamentali.map((f) => ( + + ))} + + + + {righe.map(({ g, conteggi }) => ( + + + {fondamentali.map((f) => ( + + ))} + + ))} + +
Giocatore + {f.label.slice(0, 3)} +
+ + #{g.numero} + + {g.nome} + + {conteggi[f.id].piu} + / + {conteggi[f.id].meno} +
+
+ )} +

+ Valutazioni: + positive /{" "} + − negative. La percentuale è la quota di +. +

+ + ) : null} + + {scout.azioni.length > 0 ? ( + <> +

+ Azioni registrate +

+
+ {[ + { l: "Punti", v: totaliAzioni.punti }, + { l: "Ace", v: totaliAzioni.ace }, + { l: "Muri", v: totaliAzioni.muri }, + { l: "Errori", v: totaliAzioni.errori }, + ].map((t) => ( +
+

{t.v}

+

+ {t.l} +

+
+ ))} +
+
+ {[...azioni.entries()].map(([gid, t]) => { + const g = rosa.find((x) => x.id === gid); + if (!g) return null; + return ( +
+ + #{g.numero} {g.nome} + + + {t.punti}P · {t.ace}A · {t.muri}M · {t.errori}E + +
+ ); + })} +
+ + ) : null} + + {scout.valutazioni.length === 0 && scout.azioni.length === 0 ? ( +

+ Nessun dato registrato per questa partita. +

+ ) : null} + + {admin ? ( + + ) : null} +
+ ); +} diff --git a/src/lib/scout-store.ts b/src/lib/scout-store.ts index 3eb5114..7b2f310 100644 --- a/src/lib/scout-store.ts +++ b/src/lib/scout-store.ts @@ -96,6 +96,24 @@ export function contaValutazioni( return out; } +/** + * Riepilogo delle valutazioni per la lettura nell'app: totali di squadra e per giocatore, + * su tutta la partita o su un solo set. Chi non ha valutazioni nel periodo non compare. + */ +export function sintesiValutazioni(valutazioni: Valutazione[], set?: number) { + const filtrate = set === undefined ? valutazioni : valutazioni.filter((v) => v.set === set); + const squadra = contaValutazioni([], "", undefined); + const perGiocatore = [...new Set(filtrate.map((v) => v.giocatoreId))].map((giocatoreId) => { + const c = contaValutazioni(filtrate, giocatoreId); + for (const f of fondamentali) { + squadra[f.id].piu += c[f.id].piu; + squadra[f.id].meno += c[f.id].meno; + } + return { giocatoreId, conteggi: c }; + }); + return { squadra, perGiocatore }; +} + /** * I + di un giocatore in un set, esclusa la ricezione (che qui non vale come punto messo a * terra): quello che compare nel suo riquadro dello scout. diff --git a/src/routes/partita.$id.tsx b/src/routes/partita.$id.tsx index 119cfd8..df4434a 100644 --- a/src/routes/partita.$id.tsx +++ b/src/routes/partita.$id.tsx @@ -11,8 +11,8 @@ import { DettaglioCsiEsteso, LogoSquadra, MetaPartitaCsi } from "@/components/cr import { Pagelle } from "@/components/crapp/Pagelle"; import { SondaggioCacche } from "@/components/crapp/SondaggioCacche"; import { ScoutEntry } from "@/components/crapp/ScoutEntry"; -import { haPunteggio, useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store"; -import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export"; +import { haPunteggio, useScoutMatches } from "@/lib/scout-store"; +import { ScoutReport } from "@/components/crapp/ScoutReport"; import { useIsAdmin } from "@/lib/ruoli"; import { useSonoAllenatore } from "@/lib/user-store"; import { VotazioneMvp } from "@/components/crapp/VotazioneMvp"; @@ -90,7 +90,6 @@ function PartitaDetail() { : csiMatch ? matchDaPartitaCsi(csiMatch) : undefined; - const totaliTeam = scout ? totaliSquadra([scout]) : null; const avversario = evento.titolo.includes(" vs ") ? (evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo) : evento.titolo; @@ -249,61 +248,9 @@ function PartitaDetail() { ) : null} - {scout && totaliTeam ? ( + {scout ? (
- -
- {[ - { l: "Punti", v: totaliTeam.punti }, - { l: "Ace", v: totaliTeam.ace }, - { l: "Muri", v: totaliTeam.muri }, - { l: "Errori", v: totaliTeam.errori }, - ].map((t) => ( -
-

{t.v}

-

- {t.l} -

-
- ))} -
-

- Dettaglio giocatori (uso interno allenatori) -

-
- {[...totaliPerGiocatore(scout.azioni).entries()].map(([gid, t]) => { - const g = rosa.find((x) => x.id === gid); - if (!g) return null; - return ( -
- - #{g.numero} {g.nome} - - - {t.punti}P · {t.ace}A · {t.muri}M · {t.errori}E - -
- ); - })} -
- {admin ? ( - - ) : null} -
+
) : null} diff --git a/test/unit/scout-store.test.ts b/test/unit/scout-store.test.ts index 018a694..4e29900 100644 --- a/test/unit/scout-store.test.ts +++ b/test/unit/scout-store.test.ts @@ -6,6 +6,7 @@ import { contaPiuSenzaRicezione, haPunteggio, parzialiFinali, + sintesiValutazioni, contaValutazioni, fondamentali, giocatoriConScout, @@ -197,3 +198,19 @@ assert.equal( }), true, ); + +// --- sintesi per la lettura nell'app: squadra e giocatori, partita o singolo set ----- +const tutta = sintesiValutazioni(val); +assert.deepEqual(tutta.squadra.battuta, { piu: 2, meno: 1 }); +assert.deepEqual(tutta.squadra.attacco, { piu: 1, meno: 0 }); +assert.deepEqual(tutta.squadra.ricezione, { piu: 0, meno: 1 }); +assert.deepEqual(tutta.perGiocatore.map((r) => r.giocatoreId).sort(), ["g1", "g2"]); +const set2 = sintesiValutazioni(val, 2); +assert.deepEqual( + set2.perGiocatore.map((r) => r.giocatoreId), + ["g1"], +); +assert.deepEqual(set2.squadra.muro, { piu: 1, meno: 0 }); +assert.deepEqual(set2.squadra.battuta, { piu: 0, meno: 0 }); +assert.equal(sintesiValutazioni([]).perGiocatore.length, 0); +console.log("scout-store sintesi: ok");