Scout live: valutazioni − / + per fondamentale e per set
Per ogni convocato un − o un + su battuta, ricezione, attacco e muro, separato per set e fuori dal punteggio (DD-053, migration m39: colonna scout_partite.valutazioni). Lo stato in corso le salva in scout_live.stato; il CSV admin ha la tabella per set. Nella pagina dello scout tolti il blocco avversario/campo, i tasti delle azioni e il tabellone del punteggio; il titolo della sezione mostra cognome e numero del giocatore. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
09dba1890d
commit
51b7a10c99
16 files changed
+367
-120
No files matched your search
+7
-1
@@ -13,7 +13,13 @@ versioni `0.y.z` erano pre-release.
|
||||
|
||||
## [Non rilasciato]
|
||||
|
||||
Quattro migration, da `m35` a `m38`, **non ancora applicate in produzione**.
|
||||
Cinque migration, da `m35` a `m39`, **non ancora applicate in produzione**.
|
||||
|
||||
### Aggiunto
|
||||
|
||||
- Scout live: per ogni convocato si può segnare un − o un + su battuta, ricezione, attacco e muro,
|
||||
separatamente per ogni set e senza toccare il punteggio ([DD-053](decisions/DD-053.md), `m39`). Il
|
||||
CSV admin della partita include una tabella con i conteggi per set.
|
||||
|
||||
### Modificato
|
||||
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ Colonne di `giocatori_squadra` che meritano una nota:
|
||||
| ---------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `scout_sessioni` | Chi ha il controllo dello Scout Live per una partita (blocco condiviso), una riga per evento. | Letta/scritta da `src/lib/scout-live.ts`. |
|
||||
| `scout_live` | Stato in corso (azioni non ancora concluse) di una sessione. | Solo per statistiche di squadra, mai classifiche individuali (DD-008). `src/lib/scout-stato.ts`. |
|
||||
| `scout_partite` | Archivio delle partite scoutate concluse (risultato, parziali, azioni). | Letta/scritta da `src/lib/scout-store.ts`. |
|
||||
| `scout_partite` | Archivio delle partite scoutate concluse (risultato, parziali, azioni, valutazioni). | Letta/scritta da `src/lib/scout-store.ts`. `valutazioni` (M39, DD-053): − / + per fondamentale. |
|
||||
|
||||
## Votazioni
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# DD-053 — Valutazioni − / + dei fondamentali nello Scout Live
|
||||
|
||||
**Data:** 2 novembre 2026
|
||||
**Stato:** Accettata (migration `m39`)
|
||||
|
||||
**Contesto**
|
||||
Lo scout registrava solo ciò che fa punto o lo toglie (attacco, ace, muro, errore). Mancava un
|
||||
giudizio sul resto del gioco — battuta, ricezione, attacco, muro — che non cambia il punteggio.
|
||||
|
||||
**Decisione**
|
||||
|
||||
- Per ogni convocato, un **−** o un **+** su battuta, ricezione, attacco e muro, **per set**.
|
||||
- Sono valutazioni, non azioni: lista `valutazioni` separata da `azioni`, così non entrano nel
|
||||
punteggio, nei parziali né nei totali P/A/M/E (`totaliPerGiocatore`, badge, classifiche: DD-008).
|
||||
- Ogni tocco è una riga (giocatore, fondamentale, segno, set, istante): permette l'annulla e il
|
||||
conteggio per set senza strutture da tenere allineate.
|
||||
- Stato in corso dentro `scout_live.stato` (jsonb, nessuna modifica); partita archiviata nella
|
||||
nuova colonna `scout_partite.valutazioni jsonb NOT NULL DEFAULT '[]'` (`m39`, solo additiva).
|
||||
|
||||
**Alternative scartate**
|
||||
|
||||
- Nuovi `AzioneTipo` dentro `azioni` → ogni consumatore (punteggio, totali, CSV) andrebbe filtrato
|
||||
per non contarli come punti.
|
||||
- Solo contatori aggregati per giocatore/set → niente annulla preciso, né ordine cronologico.
|
||||
- Tabella dedicata → per un dato che nasce e muore con la partita archiviata è più schema del
|
||||
necessario.
|
||||
|
||||
**Conseguenze**
|
||||
Le partite già archiviate hanno `valutazioni = []`. Le valutazioni sono visibili solo nel CSV
|
||||
admin; sono dato di squadra a uso tecnico, non entrano in classifiche individuali.
|
||||
|
||||
**Riesame**
|
||||
Se servirà confrontare le valutazioni tra partite o mostrarle ai giocatori.
|
||||
@@ -89,6 +89,7 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand
|
||||
| [DD-050](DD-050.md) | Avviso se una presenza cambia a meno di 6 ore dall'evento |
|
||||
| [DD-051](DD-051.md) | Voto MVP definitivo e MVP a pari merito |
|
||||
| [DD-052](DD-052.md) | Voto MVP segreto, nome dal database e chiusura |
|
||||
| [DD-053](DD-053.md) | Valutazioni − / + dei fondamentali nello Scout Live |
|
||||
| [DD-049](DD-049.md) | Caratteri più grandi in tutta l'app |
|
||||
| [DD-048](DD-048.md) | Via la tessera dalla dashboard |
|
||||
| [DD-047](DD-047.md) | Notifica di compleanno |
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
## Obiettivo
|
||||
|
||||
Permettere a un solo referente per volta di registrare in tempo reale, durante la partita,
|
||||
punti, ace, muri ed errori di ciascun giocatore in campo, con salvataggio condiviso su
|
||||
punti, ace, muri ed errori di ciascun giocatore in campo — più un giudizio − / + sui
|
||||
fondamentali, per set — con salvataggio condiviso su
|
||||
Supabase (non più solo `localStorage`, fix M7) così che tutta la squadra veda lo stato
|
||||
aggiornato da qualunque dispositivo.
|
||||
|
||||
@@ -21,7 +22,8 @@ aggiornato da qualunque dispositivo.
|
||||
- `scout_sessioni` — chi ha il controllo dello Scout Live per una partita (una riga per
|
||||
`evento_id`, quindi un solo detentore).
|
||||
- `scout_live` — stato in corso (azioni non ancora concluse) di una sessione.
|
||||
- `scout_partite` — archivio delle partite scoutate concluse (risultato, parziali, azioni),
|
||||
- `scout_partite` — archivio delle partite scoutate concluse (risultato, parziali, azioni,
|
||||
valutazioni − / +),
|
||||
mai più modificato una volta salvato (solo eliminabile per intero).
|
||||
- `cacche_partita` — sondaggio goliardico pre-partita, un voto per giocatore/evento
|
||||
(`UNIQUE evento_id, giocatore_id`).
|
||||
@@ -74,6 +76,21 @@ risposto). Salvataggio automatico su `scout_live` con debounce di 800ms a ogni c
|
||||
|
||||
---
|
||||
|
||||
## Valutazioni dei fondamentali (− / +)
|
||||
|
||||
Per ogni convocato lo scout può segnare un **−** o un **+** su battuta, ricezione, attacco e muro
|
||||
([DD-053](../decisions/DD-053.md)). Non sono punti: restano fuori dal punteggio, dai parziali e dai
|
||||
totali P/A/M/E. Si seleziona il giocatore (passo 1) e si tocca − o + sul fondamentale (passo 3);
|
||||
accanto a ogni tasto compare il conteggio di quel giocatore **nel set in corso**. «Annulla» nella
|
||||
sezione toglie l'ultima valutazione del set in corso (l'«Annulla» in alto resta per le azioni).
|
||||
|
||||
Ogni tocco è una riga `Valutazione` (`giocatoreId`, `fondamentale`, `segno`, `set`, `ts`) in una
|
||||
lista separata da `azioni`; i conteggi per set si calcolano con `contaValutazioni()`
|
||||
(`scout-store.ts`). Lo stato in corso le salva in `scout_live.stato.valutazioni` (assente negli
|
||||
stati salvati prima: vale lista vuota); a fine partita finiscono in `scout_partite.valutazioni`.
|
||||
|
||||
---
|
||||
|
||||
## Fine partita
|
||||
|
||||
`finePartita()` (`scout.tsx`) compone i parziali finali, inserisce la partita in
|
||||
@@ -85,8 +102,10 @@ e rilascia la sessione.
|
||||
## Export CSV
|
||||
|
||||
`scout-export.ts` genera un CSV (separatore `;`, BOM UTF-8) con parziali, riepilogo per
|
||||
giocatore e log cronologico delle azioni. Scaricabile dagli admin dalla pagina partita,
|
||||
sezione "Report tecnico".
|
||||
giocatore e log cronologico delle azioni. Se ci sono valutazioni, aggiunge una tabella per set e giocatore con i
|
||||
|
||||
- e i − di ogni fondamentale (solo per chi ne ha). Scaricabile dagli admin dalla pagina partita,
|
||||
sezione "Report tecnico".
|
||||
|
||||
---
|
||||
|
||||
@@ -127,4 +146,5 @@ calcolati a runtime).
|
||||
|
||||
## Evoluzioni possibili
|
||||
|
||||
- Mostrare le valutazioni anche nella pagina della partita (oggi solo nel CSV admin).
|
||||
- Realtime (Supabase Realtime) per aggiornare la sessione condivisa senza refresh manuale.
|
||||
@@ -536,6 +536,7 @@ export type Database = {
|
||||
parziali: Json;
|
||||
set_loro: number;
|
||||
set_nostri: number;
|
||||
valutazioni: Json;
|
||||
};
|
||||
Insert: {
|
||||
avversario: string;
|
||||
@@ -548,6 +549,7 @@ export type Database = {
|
||||
parziali?: Json;
|
||||
set_loro: number;
|
||||
set_nostri: number;
|
||||
valutazioni?: Json;
|
||||
};
|
||||
Update: {
|
||||
avversario?: string;
|
||||
@@ -560,6 +562,7 @@ export type Database = {
|
||||
parziali?: Json;
|
||||
set_loro?: number;
|
||||
set_nostri?: number;
|
||||
valutazioni?: Json;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
|
||||
+2
-1
@@ -34,7 +34,7 @@ function iniziali(nome: string, cognome: string): string {
|
||||
export function useAnagraficaRosa({
|
||||
conAllenatori = false,
|
||||
}: { conAllenatori?: boolean } = {}): Array<
|
||||
Pick<Giocatore, "id" | "nome" | "ruolo" | "numero" | "nascita">
|
||||
Pick<Giocatore, "id" | "nome" | "ruolo" | "numero" | "nascita"> & { cognome: string }
|
||||
> {
|
||||
const { righe: squadra } = useGiocatoriSquadra();
|
||||
return useMemo(
|
||||
@@ -44,6 +44,7 @@ export function useAnagraficaRosa({
|
||||
.map((g) => ({
|
||||
id: g.id,
|
||||
nome: nomeCompleto(g),
|
||||
cognome: g.cognome,
|
||||
ruolo: g.ruolo,
|
||||
numero: g.numero,
|
||||
nascita: g.nascita ?? "",
|
||||
|
||||
+33
-1
@@ -1,5 +1,11 @@
|
||||
import type { Giocatore } from "./crapp-data";
|
||||
import { azioniMeta, totaliPerGiocatore, type ScoutMatch } from "./scout-store";
|
||||
import {
|
||||
azioniMeta,
|
||||
contaValutazioni,
|
||||
fondamentali,
|
||||
totaliPerGiocatore,
|
||||
type ScoutMatch,
|
||||
} from "./scout-store";
|
||||
|
||||
/** Riga CSV con separatore ";" (Excel IT), riusata anche dall'export tesseramento. */
|
||||
export function rigaCsv(campi: Array<string | number>) {
|
||||
@@ -34,6 +40,32 @@ export function csvScoutMatch(match: ScoutMatch, rosa: Giocatore[]): string {
|
||||
if (!t) continue;
|
||||
righe.push(rigaCsv([g.numero, g.nome, g.ruolo, t.punti, t.ace, t.muri, t.errori]));
|
||||
}
|
||||
if (match.valutazioni.length > 0) {
|
||||
righe.push("");
|
||||
righe.push(
|
||||
rigaCsv([
|
||||
"Set",
|
||||
"Numero",
|
||||
"Giocatore",
|
||||
...fondamentali.flatMap((f) => [`${f.label} +`, `${f.label} -`]),
|
||||
]),
|
||||
);
|
||||
const sets = [...new Set(match.valutazioni.map((v) => v.set))].sort((x, y) => x - y);
|
||||
for (const set of sets) {
|
||||
for (const g of rosa) {
|
||||
if (!match.valutazioni.some((v) => v.set === set && v.giocatoreId === g.id)) continue;
|
||||
const c = contaValutazioni(match.valutazioni, g.id, set);
|
||||
righe.push(
|
||||
rigaCsv([
|
||||
set,
|
||||
g.numero,
|
||||
g.nome,
|
||||
...fondamentali.flatMap((f) => [c[f.id].piu, c[f.id].meno]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
righe.push("");
|
||||
righe.push(rigaCsv(["Set", "Giocatore", "Azione"]));
|
||||
for (const a of match.azioni) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import type { Azione } from "./scout-store";
|
||||
import type { Azione, Valutazione } from "./scout-store";
|
||||
|
||||
/** Stato condiviso di uno scout in corso: chi prende il controllo riparte da qui. */
|
||||
export type StatoScout = {
|
||||
azioni: Azione[];
|
||||
/** Assente negli stati salvati prima dell'introduzione delle valutazioni. */
|
||||
valutazioni?: Valutazione[];
|
||||
setChiusi: Array<[number, number]>;
|
||||
avversario: string;
|
||||
casa: boolean;
|
||||
@@ -12,6 +14,7 @@ export type StatoScout = {
|
||||
|
||||
export const statoIniziale = (avversario: string, casa: boolean): StatoScout => ({
|
||||
azioni: [],
|
||||
valutazioni: [],
|
||||
setChiusi: [],
|
||||
avversario,
|
||||
casa,
|
||||
|
||||
+41
-1
@@ -60,6 +60,42 @@ export type Azione = {
|
||||
ts: number;
|
||||
};
|
||||
|
||||
/** I fondamentali valutabili con − / + durante lo scout (non toccano il punteggio). */
|
||||
export type Fondamentale = "battuta" | "ricezione" | "attacco" | "muro";
|
||||
|
||||
export const fondamentali: Array<{ id: Fondamentale; label: string }> = [
|
||||
{ id: "battuta", label: "Battuta" },
|
||||
{ id: "ricezione", label: "Ricezione" },
|
||||
{ id: "attacco", label: "Attacco" },
|
||||
{ id: "muro", label: "Muro" },
|
||||
];
|
||||
|
||||
export type Valutazione = {
|
||||
id: string;
|
||||
giocatoreId: string;
|
||||
fondamentale: Fondamentale;
|
||||
segno: "+" | "-";
|
||||
set: number;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
export type ConteggioValutazioni = Record<Fondamentale, { piu: number; meno: number }>;
|
||||
|
||||
/** Conta i + e i − di un giocatore per fondamentale, in un set (o in tutti se `set` manca). */
|
||||
export function contaValutazioni(
|
||||
valutazioni: Valutazione[],
|
||||
giocatoreId: string,
|
||||
set?: number,
|
||||
): ConteggioValutazioni {
|
||||
const out = {} as ConteggioValutazioni;
|
||||
for (const f of fondamentali) out[f.id] = { piu: 0, meno: 0 };
|
||||
for (const v of valutazioni) {
|
||||
if (v.giocatoreId !== giocatoreId || (set !== undefined && v.set !== set)) continue;
|
||||
out[v.fondamentale][v.segno === "+" ? "piu" : "meno"] += 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export type ScoutMatch = {
|
||||
id: string;
|
||||
data: string;
|
||||
@@ -69,6 +105,7 @@ export type ScoutMatch = {
|
||||
setLoro: number;
|
||||
parziali: Array<[number, number]>;
|
||||
azioni: Azione[];
|
||||
valutazioni: Valutazione[];
|
||||
};
|
||||
|
||||
type RigaScoutPartita = {
|
||||
@@ -80,6 +117,7 @@ type RigaScoutPartita = {
|
||||
set_loro: number;
|
||||
parziali: unknown;
|
||||
azioni: unknown;
|
||||
valutazioni: unknown;
|
||||
};
|
||||
|
||||
function daRiga(r: RigaScoutPartita): ScoutMatch {
|
||||
@@ -92,6 +130,7 @@ function daRiga(r: RigaScoutPartita): ScoutMatch {
|
||||
setLoro: r.set_loro,
|
||||
parziali: (r.parziali as Array<[number, number]> | null) ?? [],
|
||||
azioni: (r.azioni as Azione[] | null) ?? [],
|
||||
valutazioni: (r.valutazioni as Valutazione[] | null) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,7 +139,7 @@ export const SCOUT_MATCHES_KEY = ["scout-partite"] as const;
|
||||
async function fetchScoutMatches(): Promise<ScoutMatch[]> {
|
||||
const { data, error } = await supabase
|
||||
.from("scout_partite")
|
||||
.select("id, data, avversario, casa, set_nostri, set_loro, parziali, azioni")
|
||||
.select("id, data, avversario, casa, set_nostri, set_loro, parziali, azioni, valutazioni")
|
||||
.order("creato_il", { ascending: false });
|
||||
if (error) throw error;
|
||||
return (data ?? []).map(daRiga);
|
||||
@@ -131,6 +170,7 @@ export function useSalvaScoutMatch() {
|
||||
set_loro: input.match.setLoro,
|
||||
parziali: JSON.parse(JSON.stringify(input.match.parziali)),
|
||||
azioni: JSON.parse(JSON.stringify(input.match.azioni)),
|
||||
valutazioni: JSON.parse(JSON.stringify(input.match.valutazioni)),
|
||||
});
|
||||
if (error) throw error;
|
||||
return input.match;
|
||||
|
||||
+81
-109
@@ -3,7 +3,6 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { Undo2, Save, CheckCircle2, Radio, Lock, CalendarX2, LogOut } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card } from "@/components/crapp/ui-bits";
|
||||
import { formatData } from "@/lib/crapp-data";
|
||||
import type { Evento } from "@/lib/eventi";
|
||||
import { useAnagraficaRosa } from "@/lib/rosa";
|
||||
@@ -27,10 +26,14 @@ import {
|
||||
} from "@/lib/scout-live";
|
||||
import {
|
||||
azioniMeta,
|
||||
contaValutazioni,
|
||||
fondamentali,
|
||||
useSalvaScoutMatch,
|
||||
totaliPerGiocatore,
|
||||
type Azione,
|
||||
type AzioneTipo,
|
||||
type Fondamentale,
|
||||
type Valutazione,
|
||||
} from "@/lib/scout-store";
|
||||
|
||||
export const Route = createFileRoute("/scout")({
|
||||
@@ -52,8 +55,6 @@ export const Route = createFileRoute("/scout")({
|
||||
component: Scout,
|
||||
});
|
||||
|
||||
const ordineAzioni: AzioneTipo[] = ["attacco", "ace", "muro", "errore"];
|
||||
|
||||
function Blocco({
|
||||
icona,
|
||||
titolo,
|
||||
@@ -196,10 +197,11 @@ function ScoutBoard({
|
||||
.trim() || "Avversario",
|
||||
partita.titolo.trim().toLowerCase().startsWith("crap"),
|
||||
);
|
||||
const [avversario, setAvversario] = useState(base.avversario);
|
||||
const [casa, setCasa] = useState(base.casa);
|
||||
const [avversario] = useState(base.avversario);
|
||||
const [casa] = useState(base.casa);
|
||||
const [setChiusi, setSetChiusi] = useState<Array<[number, number]>>(base.setChiusi);
|
||||
const [azioni, setAzioni] = useState<Azione[]>(base.azioni);
|
||||
const [valutazioni, setValutazioni] = useState<Valutazione[]>(base.valutazioni ?? []);
|
||||
const [selezionato, setSelezionato] = useState<string | null>(null);
|
||||
const salva = useSalvaStatoScout();
|
||||
const cancella = useCancellaStatoScout();
|
||||
@@ -223,37 +225,44 @@ function ScoutBoard({
|
||||
const id = window.setTimeout(() => {
|
||||
void salva.mutateAsync({
|
||||
eventoId: partita.id,
|
||||
stato: { azioni, setChiusi, avversario, casa },
|
||||
stato: { azioni, valutazioni, setChiusi, avversario, casa },
|
||||
});
|
||||
}, 800);
|
||||
return () => window.clearTimeout(id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [azioni, setChiusi, avversario, casa, partita.id]);
|
||||
}, [azioni, valutazioni, setChiusi, avversario, casa, partita.id]);
|
||||
|
||||
const setCorrente = setChiusi.length + 1;
|
||||
const azioniSet = azioni.filter((a) => a.set === setCorrente);
|
||||
const puntiNoi = azioniSet.filter((a) => azioniMeta[a.tipo].nostro).length;
|
||||
const puntiLoro = azioniSet.length - puntiNoi;
|
||||
const setNostri = setChiusi.filter(([n, l]) => n > l).length;
|
||||
const setLoro = setChiusi.length - setNostri;
|
||||
const totali = useMemo(() => totaliPerGiocatore(azioni), [azioni]);
|
||||
|
||||
function registra(tipo: AzioneTipo) {
|
||||
const meta = azioniMeta[tipo];
|
||||
if (meta.richiedeGiocatore && !selezionato) {
|
||||
function valuta(fondamentale: Fondamentale, segno: Valutazione["segno"]) {
|
||||
if (!selezionato) {
|
||||
toast.error("Seleziona prima il giocatore");
|
||||
return;
|
||||
}
|
||||
setAzioni((prev) => {
|
||||
const nuova: Azione = {
|
||||
setValutazioni((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `${Date.now()}-${prev.length}`,
|
||||
tipo,
|
||||
giocatoreId: selezionato,
|
||||
fondamentale,
|
||||
segno,
|
||||
set: setCorrente,
|
||||
ts: Date.now(),
|
||||
...(meta.richiedeGiocatore && selezionato ? { giocatoreId: selezionato } : {}),
|
||||
};
|
||||
return [...prev, nuova];
|
||||
});
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
function annullaValutazione() {
|
||||
const ultima = valutazioni[valutazioni.length - 1];
|
||||
if (!ultima || ultima.set !== setCorrente) {
|
||||
toast.error("Puoi annullare solo le valutazioni del set in corso");
|
||||
return;
|
||||
}
|
||||
setValutazioni((prev) => prev.slice(0, -1));
|
||||
}
|
||||
|
||||
function annulla() {
|
||||
@@ -294,6 +303,7 @@ function ScoutBoard({
|
||||
setLoro: parziali.length - vinti,
|
||||
parziali,
|
||||
azioni,
|
||||
valutazioni,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
@@ -307,6 +317,10 @@ function ScoutBoard({
|
||||
navigate({ to: "/squadra" });
|
||||
}
|
||||
|
||||
const giocatoreSel = rosa.find((g) => g.id === selezionato);
|
||||
const contiSelezionato = selezionato
|
||||
? contaValutazioni(valutazioni, selezionato, setCorrente)
|
||||
: null;
|
||||
const ultime = [...azioni].slice(-4).reverse();
|
||||
|
||||
return (
|
||||
@@ -334,35 +348,6 @@ function ScoutBoard({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-center">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[13px] uppercase text-primary-foreground/60">CRAP Volley</p>
|
||||
<p className="font-display text-5xl leading-none text-accent">{puntiNoi}</p>
|
||||
</div>
|
||||
<p className="font-display text-2xl leading-none text-primary-foreground/50">
|
||||
{setNostri}-{setLoro}
|
||||
</p>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[13px] uppercase text-primary-foreground/60">
|
||||
{avversario || "Avversario"}
|
||||
</p>
|
||||
<p className="font-display text-5xl leading-none">{puntiLoro}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{setChiusi.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap justify-center gap-1.5">
|
||||
{setChiusi.map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="rounded-lg bg-primary-foreground/10 px-2 py-0.5 text-[13px] font-semibold tabular-nums"
|
||||
>
|
||||
{p[0]}-{p[1]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<section className="px-5 pt-4">
|
||||
@@ -400,39 +385,55 @@ function ScoutBoard({
|
||||
</section>
|
||||
|
||||
<section className="px-5 pt-4">
|
||||
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
2. Tocca l'azione
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{ordineAzioni.map((tipo) => (
|
||||
<button
|
||||
key={tipo}
|
||||
type="button"
|
||||
onClick={() => registra(tipo)}
|
||||
className={cn(
|
||||
"rounded-2xl py-4 font-display text-xl uppercase tracking-wide shadow-card transition-transform active:scale-95",
|
||||
azioniMeta[tipo].className,
|
||||
!selezionato && "opacity-50",
|
||||
)}
|
||||
>
|
||||
{azioniMeta[tipo].label}
|
||||
</button>
|
||||
))}
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
2. Valuta{giocatoreSel ? ` ${giocatoreSel.cognome} - ${giocatoreSel.numero}` : ""} · Set{" "}
|
||||
{setCorrente}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={annullaValutazione}
|
||||
disabled={!valutazioni.some((v) => v.set === setCorrente)}
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-secondary px-3 py-1.5 text-[13px] font-bold disabled:opacity-40"
|
||||
>
|
||||
<Undo2 className="h-3.5 w-3.5" /> Annulla
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
{(["punto_avv", "errore_avv"] as AzioneTipo[]).map((tipo) => (
|
||||
<button
|
||||
key={tipo}
|
||||
type="button"
|
||||
onClick={() => registra(tipo)}
|
||||
className={cn(
|
||||
"rounded-2xl py-3 text-[15px] font-bold uppercase shadow-card transition-transform active:scale-95",
|
||||
azioniMeta[tipo].className,
|
||||
)}
|
||||
>
|
||||
{azioniMeta[tipo].short}
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-2 rounded-3xl bg-card p-3 shadow-card",
|
||||
!selezionato && "opacity-60",
|
||||
)}
|
||||
>
|
||||
{!selezionato ? (
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Tocca un giocatore: i + e i − non cambiano il punteggio.
|
||||
</p>
|
||||
) : null}
|
||||
{fondamentali.map((f) => {
|
||||
const c = contiSelezionato?.[f.id] ?? { piu: 0, meno: 0 };
|
||||
return (
|
||||
<div key={f.id} className="grid grid-cols-[1fr_auto_auto] items-center gap-2">
|
||||
<span className="text-[15px] font-bold">{f.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${f.label}: meno`}
|
||||
onClick={() => valuta(f.id, "-")}
|
||||
className="min-w-16 rounded-2xl bg-destructive px-4 py-2.5 font-display text-xl leading-none text-destructive-foreground shadow-card transition-transform active:scale-95"
|
||||
>
|
||||
− <span className="tabular-nums">{c.meno}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${f.label}: più`}
|
||||
onClick={() => valuta(f.id, "+")}
|
||||
className="min-w-16 rounded-2xl bg-success px-4 py-2.5 font-display text-xl leading-none text-success-foreground shadow-card transition-transform active:scale-95"
|
||||
>
|
||||
+ <span className="tabular-nums">{c.piu}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -472,35 +473,6 @@ function ScoutBoard({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-5 pt-4">
|
||||
<Card>
|
||||
<label className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||
Avversario
|
||||
</label>
|
||||
<input
|
||||
value={avversario}
|
||||
maxLength={40}
|
||||
onChange={(e) => setAvversario(e.target.value)}
|
||||
className="mt-1 w-full rounded-xl border border-border bg-background px-3 py-2 text-[15px]"
|
||||
/>
|
||||
<div className="mt-3 flex rounded-full bg-secondary p-1">
|
||||
{[true, false].map((v) => (
|
||||
<button
|
||||
key={String(v)}
|
||||
type="button"
|
||||
onClick={() => setCasa(v)}
|
||||
className={cn(
|
||||
"flex-1 rounded-full py-1.5 text-[13px] font-bold uppercase",
|
||||
casa === v ? "bg-card text-foreground shadow-card" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{v ? "In casa" : "Trasferta"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-2 gap-2 px-5 pb-4 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- M39 — Scout Live: valutazioni − / + per fondamentale.
|
||||
--
|
||||
-- Durante la partita lo scout può segnare, per ogni giocatore e per ogni set, un − o un +
|
||||
-- su battuta, ricezione, attacco e muro. Non sono punti: restano fuori dal punteggio e da
|
||||
-- `azioni`. Lo stato in corso le porta dentro `scout_live.stato` (jsonb, nessuna modifica);
|
||||
-- la partita archiviata le conserva in questa colonna, accanto alle azioni.
|
||||
-- Solo additiva: le partite già salvate ricevono l'elenco vuoto.
|
||||
|
||||
ALTER TABLE public.scout_partite
|
||||
ADD COLUMN valutazioni jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
@@ -32,6 +32,7 @@ if (!locale) {
|
||||
"turni_palloni",
|
||||
"risposte_presenze",
|
||||
"scout_live",
|
||||
"scout_partite",
|
||||
] as const;
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
@@ -312,6 +313,32 @@ if (!locale) {
|
||||
"il jsonb viene rimpiazzato: i campi del set precedente non restano appesi",
|
||||
);
|
||||
});
|
||||
|
||||
// --- scout: le valutazioni − / + stanno accanto alle azioni (m39) ----------------
|
||||
await prova(
|
||||
"la partita scoutata conserva le valutazioni; senza, l'elenco è vuoto",
|
||||
async () => {
|
||||
const evento = `${PREFISSO}-e5`;
|
||||
const base = {
|
||||
evento_id: evento,
|
||||
data: "2026-11-02",
|
||||
avversario: "X",
|
||||
set_nostri: 3,
|
||||
set_loro: 0,
|
||||
};
|
||||
await upsert("scout_partite", "id", { ...base, id: `${PREFISSO}-s1` });
|
||||
const valutazioni = [
|
||||
{ id: "v1", giocatoreId: "g1", fondamentale: "battuta", segno: "+", set: 1, ts: 0 },
|
||||
];
|
||||
await upsert("scout_partite", "id", { ...base, id: `${PREFISSO}-s2`, valutazioni });
|
||||
const righe = await leggi(
|
||||
"scout_partite",
|
||||
`evento_id=eq.${evento}&select=id,valutazioni&order=id`,
|
||||
);
|
||||
assert.deepEqual(righe[0]?.["valutazioni"], [], "le partite senza valutazioni hanno []");
|
||||
assert.deepEqual(righe[1]?.["valutazioni"], valutazioni);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
for (const tabella of TABELLE) {
|
||||
const colonna = tabella.endsWith("_voti") ? "match_id" : "evento_id";
|
||||
|
||||
@@ -29,6 +29,7 @@ const match: ScoutMatch = {
|
||||
[25, 18],
|
||||
],
|
||||
azioni: [a("attacco", g1.id), a("ace", g1.id, 2), a("errore", g2.id, 2), a("punto_avv")],
|
||||
valutazioni: [],
|
||||
};
|
||||
|
||||
const csv = csvScoutMatch(match, giocatori);
|
||||
@@ -78,3 +79,42 @@ const conVirgolette = csvScoutMatch({ ...match, avversario: 'Team "X"' }, giocat
|
||||
assert.ok(conVirgolette.includes('"Team ""X"""'));
|
||||
|
||||
console.log("scout-export: ok");
|
||||
|
||||
// --- valutazioni − / + per set -------------------------------------------------
|
||||
assert.ok(!csv.includes("Battuta +"), "senza valutazioni la sezione non c'è");
|
||||
const val = (
|
||||
giocatoreId: string,
|
||||
fondamentale: "battuta" | "muro",
|
||||
segno: "+" | "-",
|
||||
set: number,
|
||||
) => ({
|
||||
id: `${giocatoreId}-${fondamentale}-${segno}-${set}-${Math.random()}`,
|
||||
giocatoreId,
|
||||
fondamentale,
|
||||
segno,
|
||||
set,
|
||||
ts: 0,
|
||||
});
|
||||
const conVal = csvScoutMatch(
|
||||
{
|
||||
...match,
|
||||
valutazioni: [
|
||||
val(g1.id, "battuta", "+", 1),
|
||||
val(g1.id, "battuta", "+", 1),
|
||||
val(g1.id, "battuta", "-", 1),
|
||||
val(g1.id, "muro", "-", 2),
|
||||
],
|
||||
},
|
||||
giocatori,
|
||||
);
|
||||
assert.ok(
|
||||
conVal.includes(
|
||||
"Set;Numero;Giocatore;Battuta +;Battuta -;Ricezione +;Ricezione -;Attacco +;Attacco -;Muro +;Muro -",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
conVal.includes(`1;${g1.numero};${g1.nome};2;1;0;0;0;0;0;0`),
|
||||
"set 1: 2 più e 1 meno in battuta",
|
||||
);
|
||||
assert.ok(conVal.includes(`2;${g1.numero};${g1.nome};0;0;0;0;0;0;0;1`), "set 2: 1 meno a muro");
|
||||
assert.ok(!conVal.includes(`;${g2.nome};0;0;0;0;0;0;0;0`), "chi non ha valutazioni non ha riga");
|
||||
@@ -5,7 +5,10 @@ import { SCOUT_STATO_KEY, statoIniziale } from "@/lib/scout-stato";
|
||||
// --- statoIniziale: parte vuoto, con avversario e campo memorizzati -----------
|
||||
const stato = statoIniziale("Avversari", true);
|
||||
// Confronta una copia: deepEqual restringerebbe `stato` al letterale, con `azioni: never[]`.
|
||||
assert.deepEqual({ ...stato }, { azioni: [], setChiusi: [], avversario: "Avversari", casa: true });
|
||||
assert.deepEqual(
|
||||
{ ...stato },
|
||||
{ azioni: [], valutazioni: [], setChiusi: [], avversario: "Avversari", casa: true },
|
||||
);
|
||||
|
||||
const trasferta = statoIniziale("Altra Squadra", false);
|
||||
assert.equal(trasferta.casa, false);
|
||||
@@ -14,6 +17,7 @@ assert.deepEqual(trasferta.azioni, [], "ogni chiamata parte da una lista vuota i
|
||||
// Le liste non devono essere condivise tra due stati distinti.
|
||||
stato.azioni.push({ id: "a1", tipo: "attacco", set: 1, ts: 0 });
|
||||
assert.deepEqual(trasferta.azioni, [], "modificare uno stato non tocca l'altro");
|
||||
assert.notEqual(stato.valutazioni, trasferta.valutazioni, "anche le valutazioni sono indipendenti");
|
||||
|
||||
// --- SCOUT_STATO_KEY: chiave stabile per React Query --------------------------
|
||||
assert.deepEqual(SCOUT_STATO_KEY("evt-1"), ["scout-stato", "evt-1"]);
|
||||
|
||||
@@ -3,10 +3,13 @@ import assert from "node:assert/strict";
|
||||
import { giocatori } from "@/lib/crapp-data";
|
||||
import {
|
||||
azioniMeta,
|
||||
contaValutazioni,
|
||||
fondamentali,
|
||||
giocatoriConScout,
|
||||
totaliPerGiocatore,
|
||||
totaliSquadra,
|
||||
type Azione,
|
||||
type Valutazione,
|
||||
type ScoutMatch,
|
||||
} from "@/lib/scout-store";
|
||||
|
||||
@@ -32,6 +35,7 @@ const match = (
|
||||
setLoro,
|
||||
parziali: [],
|
||||
azioni,
|
||||
valutazioni: [],
|
||||
});
|
||||
|
||||
// --- totaliPerGiocatore: ace e muri valgono anche come punto ------------------
|
||||
@@ -84,3 +88,54 @@ for (const [tipo, meta] of Object.entries(azioniMeta)) {
|
||||
}
|
||||
|
||||
console.log("scout-store: ok");
|
||||
|
||||
// --- valutazioni − / +: per giocatore, fondamentale e set; fuori dal punteggio -------
|
||||
const v = (
|
||||
giocatoreId: string,
|
||||
fondamentale: Valutazione["fondamentale"],
|
||||
segno: Valutazione["segno"],
|
||||
set = 1,
|
||||
): Valutazione => ({
|
||||
id: `${giocatoreId}-${fondamentale}-${segno}-${set}-${Math.random()}`,
|
||||
giocatoreId,
|
||||
fondamentale,
|
||||
segno,
|
||||
set,
|
||||
ts: 0,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
fondamentali.map((f) => f.id),
|
||||
["battuta", "ricezione", "attacco", "muro"],
|
||||
);
|
||||
const val = [
|
||||
v("g1", "battuta", "+"),
|
||||
v("g1", "battuta", "+"),
|
||||
v("g1", "battuta", "-"),
|
||||
v("g1", "ricezione", "-", 2),
|
||||
v("g1", "muro", "+", 2),
|
||||
v("g2", "attacco", "+"),
|
||||
];
|
||||
assert.deepEqual(contaValutazioni(val, "g1", 1), {
|
||||
battuta: { piu: 2, meno: 1 },
|
||||
ricezione: { piu: 0, meno: 0 },
|
||||
attacco: { piu: 0, meno: 0 },
|
||||
muro: { piu: 0, meno: 0 },
|
||||
});
|
||||
assert.deepEqual(
|
||||
contaValutazioni(val, "g1", 2).ricezione,
|
||||
{ piu: 0, meno: 1 },
|
||||
"il set 2 è separato",
|
||||
);
|
||||
assert.deepEqual(
|
||||
contaValutazioni(val, "g1").battuta,
|
||||
{ piu: 2, meno: 1 },
|
||||
"senza set: tutta la partita",
|
||||
);
|
||||
assert.deepEqual(contaValutazioni(val, "g1").ricezione, { piu: 0, meno: 1 });
|
||||
assert.deepEqual(
|
||||
contaValutazioni(val, "g3", 1).battuta,
|
||||
{ piu: 0, meno: 0 },
|
||||
"giocatore senza valutazioni",
|
||||
);
|
||||
assert.equal(totaliPerGiocatore([]).size, 0, "le valutazioni non sono azioni: nessun punto");
|
||||
Reference in new issue
Block a user