Sondaggio cacche: classifica completa per tutti i giocatori

Elenco con posizioni a pari merito senza salti, barra proporzionale
e riga personale evidenziata, anche a sondaggio chiuso.
Sezione «Report tecnico» della partita rinominata «Scout».

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-11-06 23:25:50 +01:00
1 parent a7b9affbd6
commit 91ac7ce65c
4 files changed
+76 -19

No files matched your search

+6
View File
@@ -13,6 +13,12 @@ versioni `0.y.z` erano pre-release.
## [Non rilasciato] ## [Non rilasciato]
### Modificato
- Sondaggio cacche: la classifica completa è visibile a tutti i giocatori, anche a sondaggio
chiuso, con posizioni a pari merito senza salti (1°, 1°, 2°) e la propria riga evidenziata.
- Pagina partita: la sezione «Report tecnico» si chiama «Scout».
## [1.6.0] - 2026-11-02 ## [1.6.0] - 2026-11-02
Voto MVP definitivo e segreto, Scout live a valutazioni. Cinque migration, da `m35` a `m39`, Voto MVP definitivo e segreto, Scout live a valutazioni. Cinque migration, da `m35` a `m39`,
+3 -3
View File
@@ -109,7 +109,7 @@ Senza punteggio, `chiudiSet()` chiude il set (come 0-0) se ha almeno una valutaz
parziali se ha dati (`parzialiFinali()`, `scout-store.ts`). Con nessuna valutazione risponde parziali se ha dati (`parzialiFinali()`, `scout-store.ts`). Con nessuna valutazione risponde
«Nessun set da salvare». Una partita senza punti ha parziali 0-0 e **non è un risultato**: «Nessun set da salvare». Una partita senza punti ha parziali 0-0 e **non è un risultato**:
`haPunteggio()` la esclude da Home, Classifica e dalla pagina della partita, dove non sostituisce il `haPunteggio()` la esclude da Home, Classifica e dalla pagina della partita, dove non sostituisce il
referto CSI; resta visibile il «Report tecnico» con il CSV. referto CSI; resta visibile il «Scout» con il CSV.
`finePartita()` (`scout.tsx`) compone i parziali finali, inserisce la partita in `finePartita()` (`scout.tsx`) compone i parziali finali, inserisce la partita in
`scout_partite` (INSERT, non upsert), poi cancella la riga da `scout_live` (stato consumato) `scout_partite` (INSERT, non upsert), poi cancella la riga da `scout_live` (stato consumato)
@@ -122,7 +122,7 @@ e rilascia la sessione.
`scout-export.ts` genera un CSV (separatore `;`, BOM UTF-8) con parziali, riepilogo per giocatore e `scout-export.ts` genera un CSV (separatore `;`, BOM UTF-8) con parziali, riepilogo per giocatore e
log cronologico delle azioni. Se ci sono valutazioni, aggiunge una tabella per set e giocatore con log cronologico delle azioni. Se ci sono valutazioni, aggiunge una tabella per set e giocatore con
i conteggi di «più» e «meno» di ogni fondamentale (solo per chi ne ha). Scaricabile dagli admin dalla i conteggi di «più» e «meno» di ogni fondamentale (solo per chi ne ha). Scaricabile dagli admin dalla
pagina partita, sezione "Report tecnico". pagina partita, sezione "Scout".
--- ---
@@ -156,7 +156,7 @@ calcolati a runtime).
- Le valutazioni dei set chiusi e delle partite finite sono salvate ma non si rivedono nell'app: lo - 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 scout mostra solo il set in corso e l'unica lettura è il CSV admin. Evoluzione: tabella nella
pagina partita. pagina partita.
- Il blocco «Report tecnico» della pagina partita mostra ancora Punti/Ace/Muri/Errori (a zero per le - 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. partite di sole valutazioni) e non le valutazioni, che sono solo nel CSV admin.
- Scout aperto a tutta la squadra: nessun filtro su chi può registrare le azioni, l'unica - Scout aperto a tutta la squadra: nessun filtro su chi può registrare le azioni, l'unica
garanzia è il lock di sessione (vedi sopra). garanzia è il lock di sessione (vedi sopra).
+66 -15
View File
@@ -40,11 +40,15 @@ export function SondaggioCacche({
const media = mediaPartita(righe, eventoId); const media = mediaPartita(righe, eventoId);
const classifica = [...dellaPartita] const classifica = [...dellaPartita]
.sort((a, b) => b.quantita - a.quantita) .sort((a, b) => b.quantita - a.quantita)
.slice(0, 3)
.map((r) => { .map((r) => {
const g = squadra.find((g) => g.id === r.giocatore_id); const g = squadra.find((g) => g.id === r.giocatore_id);
return { ...r, nome: g ? nomeCompleto(g) : "—" }; return { ...r, nome: g ? nomeCompleto(g) : "—" };
}); })
// Pari merito = stessa posizione, senza salti (1°, 1°, 2°).
.map((r, _, tutti) => ({
...r,
pos: new Set(tutti.filter((t) => t.quantita > r.quantita).map((t) => t.quantita)).size + 1,
}));
async function rispondi(quantita: number) { async function rispondi(quantita: number) {
if (!io) return; if (!io) return;
@@ -89,6 +93,7 @@ export function SondaggioCacche({
? "Sondaggio chiuso: era aperto fino al fischio d'inizio." ? "Sondaggio chiuso: era aperto fino al fischio d'inizio."
: "Apre alle 8:00 del giorno della partita: riceverai una notifica."} : "Apre alle 8:00 del giorno della partita: riceverai una notifica."}
</p> </p>
<Classifica righe={classifica} io={io?.id} media={media} />
</Card> </Card>
); );
} }
@@ -126,19 +131,7 @@ export function SondaggioCacche({
))} ))}
</div> </div>
<div className="mt-3 flex flex-wrap items-center gap-2 text-[13px]"> <Classifica righe={classifica} io={io?.id} media={media} />
<span className="rounded-full bg-secondary px-2.5 py-1 font-semibold">
Media squadra {media}
</span>
{classifica.map((r, i) => (
<span
key={r.giocatore_id}
className="rounded-full bg-secondary px-2.5 py-1 font-semibold"
>
{i === 0 ? "🥇" : i === 1 ? "🥈" : "🥉"} {r.nome} · {r.quantita}
</span>
))}
</div>
{admin ? ( {admin ? (
<button <button
@@ -154,3 +147,61 @@ export function SondaggioCacche({
</Card> </Card>
); );
} }
const MEDAGLIE = ["🥇", "🥈", "🥉"];
type RigaClassifica = { giocatore_id: string; nome: string; quantita: number; pos: number };
function Classifica({
righe,
io,
media,
}: {
righe: RigaClassifica[];
io: string | undefined;
media: number | string;
}) {
if (righe.length === 0) return null;
const max = Math.max(...righe.map((r) => r.quantita), 1);
return (
<div className="mt-4">
<div className="mb-2 flex items-center justify-between text-[13px]">
<span className="font-bold uppercase tracking-wide text-muted-foreground">Classifica</span>
<span className="rounded-full bg-secondary px-2.5 py-1 font-semibold">
Media squadra {media}
</span>
</div>
<ol className="divide-y divide-border overflow-hidden rounded-2xl bg-secondary/60">
{righe.map((r) => (
<li
key={r.giocatore_id}
className={cn(
"relative flex items-center gap-3 px-3 py-2.5",
r.giocatore_id === io && "bg-accent/15",
)}
>
<span
aria-hidden
className="absolute inset-y-0 left-0 bg-accent/10"
style={{ width: `${(r.quantita / max) * 100}%` }}
/>
<span className="relative w-7 text-center text-[15px] font-bold tabular-nums text-muted-foreground">
{r.pos <= 3 ? MEDAGLIE[r.pos - 1] : `${r.pos}°`}
</span>
<span className="relative min-w-0 flex-1 truncate text-[15px] font-semibold">
{r.nome}
{r.giocatore_id === io ? (
<span className="ml-1.5 text-[13px] font-bold uppercase text-muted-foreground">
tu
</span>
) : null}
</span>
<span className="relative text-[17px] font-bold tabular-nums">
{r.quantita === 5 ? "5+" : r.quantita}
</span>
</li>
))}
</ol>
</div>
);
}
+1 -1
View File
@@ -250,7 +250,7 @@ function PartitaDetail() {
) : null} ) : null}
{scout && totaliTeam ? ( {scout && totaliTeam ? (
<Section titolo="Report tecnico"> <Section titolo="Scout">
<Card> <Card>
<div className="grid grid-cols-4 gap-2 text-center"> <div className="grid grid-cols-4 gap-2 text-center">
{[ {[