La sezione «Scout» mostra le valutazioni − / + nell'app, non solo nel CSV: percentuale di + e conteggi per fondamentale, selettore Totale / Set 1-5, tabella per giocatore e guida (i) su come leggere i dati. Le azioni (punti, ace, muri, errori) restano in un blocco a parte per le partite scoutate col metodo precedente. Il CSV resta per gli admin. Aggiunge sintesiValutazioni() in scout-store con test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
313 lines
9.1 KiB
TypeScript
313 lines
9.1 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { supabase } from "@/integrations/supabase/client";
|
||
import { giocatori, type Giocatore } from "./crapp-data";
|
||
|
||
export type AzioneTipo = "attacco" | "ace" | "muro" | "errore" | "punto_avv" | "errore_avv";
|
||
|
||
export const azioniMeta: Record<
|
||
AzioneTipo,
|
||
{ label: string; short: string; nostro: boolean; richiedeGiocatore: boolean; className: string }
|
||
> = {
|
||
attacco: {
|
||
label: "Punto attacco",
|
||
short: "Punto",
|
||
nostro: true,
|
||
richiedeGiocatore: true,
|
||
className: "bg-accent text-accent-foreground",
|
||
},
|
||
ace: {
|
||
label: "Ace",
|
||
short: "Ace",
|
||
nostro: true,
|
||
richiedeGiocatore: true,
|
||
className: "bg-success text-success-foreground",
|
||
},
|
||
muro: {
|
||
label: "Muro",
|
||
short: "Muro",
|
||
nostro: true,
|
||
richiedeGiocatore: true,
|
||
className: "bg-info text-info-foreground",
|
||
},
|
||
errore: {
|
||
label: "Errore nostro",
|
||
short: "Errore",
|
||
nostro: false,
|
||
richiedeGiocatore: true,
|
||
className: "bg-destructive text-destructive-foreground",
|
||
},
|
||
punto_avv: {
|
||
label: "Punto avversario",
|
||
short: "Punto avv.",
|
||
nostro: false,
|
||
richiedeGiocatore: false,
|
||
className: "bg-muted text-muted-foreground",
|
||
},
|
||
errore_avv: {
|
||
label: "Errore avversario",
|
||
short: "Err. avv.",
|
||
nostro: true,
|
||
richiedeGiocatore: false,
|
||
className: "bg-secondary text-foreground",
|
||
},
|
||
};
|
||
|
||
export type Azione = {
|
||
id: string;
|
||
tipo: AzioneTipo;
|
||
giocatoreId?: string;
|
||
set: number;
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
export function contaPiuSenzaRicezione(
|
||
valutazioni: Valutazione[],
|
||
giocatoreId: string,
|
||
set: number,
|
||
): number {
|
||
return valutazioni.filter(
|
||
(v) =>
|
||
v.giocatoreId === giocatoreId &&
|
||
v.set === set &&
|
||
v.segno === "+" &&
|
||
v.fondamentale !== "ricezione",
|
||
).length;
|
||
}
|
||
|
||
/**
|
||
* Parziali da archiviare: i set chiusi più il set in corso, se ha qualcosa dentro (punti o
|
||
* valutazioni). Senza punteggio un set vale 0-0: serve solo a numerarlo.
|
||
*/
|
||
export function parzialiFinali(
|
||
setChiusi: Array<[number, number]>,
|
||
corrente: [number, number],
|
||
haDati: boolean,
|
||
): Array<[number, number]> {
|
||
return haDati ? [...setChiusi, corrente] : setChiusi;
|
||
}
|
||
|
||
export type ScoutMatch = {
|
||
id: string;
|
||
data: string;
|
||
avversario: string;
|
||
casa: boolean;
|
||
setNostri: number;
|
||
setLoro: number;
|
||
parziali: Array<[number, number]>;
|
||
azioni: Azione[];
|
||
valutazioni: Valutazione[];
|
||
};
|
||
|
||
type RigaScoutPartita = {
|
||
id: string;
|
||
data: string;
|
||
avversario: string;
|
||
casa: boolean;
|
||
set_nostri: number;
|
||
set_loro: number;
|
||
parziali: unknown;
|
||
azioni: unknown;
|
||
valutazioni: unknown;
|
||
};
|
||
|
||
function daRiga(r: RigaScoutPartita): ScoutMatch {
|
||
return {
|
||
id: r.id,
|
||
data: r.data,
|
||
avversario: r.avversario,
|
||
casa: r.casa,
|
||
setNostri: r.set_nostri,
|
||
setLoro: r.set_loro,
|
||
parziali: (r.parziali as Array<[number, number]> | null) ?? [],
|
||
azioni: (r.azioni as Azione[] | null) ?? [],
|
||
valutazioni: (r.valutazioni as Valutazione[] | null) ?? [],
|
||
};
|
||
}
|
||
|
||
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, valutazioni")
|
||
.order("creato_il", { ascending: false });
|
||
if (error) throw error;
|
||
return (data ?? []).map(daRiga);
|
||
}
|
||
|
||
/** Partite scoutate condivise con tutta la squadra: chi scoutizza le vede da qualsiasi
|
||
* dispositivo, non solo da quello di chi ha chiuso la partita. */
|
||
export function useScoutMatches(): ScoutMatch[] {
|
||
const { data } = useQuery({
|
||
queryKey: SCOUT_MATCHES_KEY,
|
||
staleTime: 60_000,
|
||
queryFn: fetchScoutMatches,
|
||
});
|
||
return data ?? [];
|
||
}
|
||
|
||
export function useSalvaScoutMatch() {
|
||
const queryClient = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: async (input: { eventoId: string | null; match: ScoutMatch }) => {
|
||
const { error } = await supabase.from("scout_partite").insert({
|
||
id: input.match.id,
|
||
evento_id: input.eventoId,
|
||
data: input.match.data,
|
||
avversario: input.match.avversario,
|
||
casa: input.match.casa,
|
||
set_nostri: input.match.setNostri,
|
||
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;
|
||
},
|
||
// La query ordina per `creato_il` decrescente: la partita appena salvata è la più recente.
|
||
onSuccess: (match) =>
|
||
queryClient.setQueryData<ScoutMatch[]>(SCOUT_MATCHES_KEY, (prec) => [
|
||
match,
|
||
...(prec ?? []).filter((m) => m.id !== match.id),
|
||
]),
|
||
});
|
||
}
|
||
|
||
export function useEliminaScoutMatch() {
|
||
const queryClient = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: async (id: string) => {
|
||
const { error } = await supabase.from("scout_partite").delete().eq("id", id);
|
||
if (error) throw error;
|
||
return id;
|
||
},
|
||
onSuccess: (id) =>
|
||
queryClient.setQueryData<ScoutMatch[]>(SCOUT_MATCHES_KEY, (prec) =>
|
||
(prec ?? []).filter((m) => m.id !== id),
|
||
),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Una partita scoutata ha un risultato solo se ha registrato punti: quelle fatte di sole
|
||
* valutazioni − / + hanno parziali 0-0 e non devono sostituire il referto ufficiale né
|
||
* comparire come risultato in Home e Classifica.
|
||
*/
|
||
export function haPunteggio(match: Pick<ScoutMatch, "parziali">): boolean {
|
||
return match.parziali.some(([n, l]) => n + l > 0);
|
||
}
|
||
|
||
/** Somma delle azioni di un match per giocatore. */
|
||
export function totaliPerGiocatore(azioni: Azione[]) {
|
||
const out = new Map<string, { punti: number; ace: number; muri: number; errori: number }>();
|
||
for (const a of azioni) {
|
||
if (!a.giocatoreId) continue;
|
||
const cur = out.get(a.giocatoreId) ?? { punti: 0, ace: 0, muri: 0, errori: 0 };
|
||
if (a.tipo === "attacco") cur.punti += 1;
|
||
if (a.tipo === "ace") {
|
||
cur.ace += 1;
|
||
cur.punti += 1;
|
||
}
|
||
if (a.tipo === "muro") {
|
||
cur.muri += 1;
|
||
cur.punti += 1;
|
||
}
|
||
if (a.tipo === "errore") cur.errori += 1;
|
||
out.set(a.giocatoreId, cur);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** Totali di squadra ricavati dallo scout (dato tecnico, non personale). */
|
||
export function totaliSquadra(matches: ScoutMatch[]) {
|
||
const out = { punti: 0, ace: 0, muri: 0, errori: 0 };
|
||
for (const m of matches) {
|
||
for (const t of totaliPerGiocatore(m.azioni).values()) {
|
||
out.punti += t.punti;
|
||
out.ace += t.ace;
|
||
out.muri += t.muri;
|
||
out.errori += t.errori;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* MVP ricavati dalle partite scoutate (mappa matchId → nome vincitore).
|
||
* Le presenze personali restano su `risposte_presenze`, non sullo scout.
|
||
*/
|
||
export function giocatoriConScout(
|
||
matches: ScoutMatch[],
|
||
mvpPerMatch: Record<string, string> = {},
|
||
): Giocatore[] {
|
||
return giocatori.map((g) => {
|
||
let mvp = 0;
|
||
for (const m of matches) {
|
||
if (mvpPerMatch[m.id] === g.nome) mvp += 1;
|
||
}
|
||
return {
|
||
...g,
|
||
mvp,
|
||
};
|
||
});
|
||
}
|