Files
CRAPP/src/lib/scout-store.ts
T
davideandClaude Sonnet 5.5 30fb8e23b6 Scout: risultati leggibili nella pagina partita
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>
2026-11-06 23:38:35 +01:00

313 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
});
}