2026-09-01 16:08:59 +02:00
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
|
import { supabase } from "@/integrations/supabase/client";
|
2026-09-01 13:38:33 +02:00
|
|
|
|
import { giocatori, type Giocatore } from "./crapp-data";
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
2026-09-01 13:38:33 +02:00
|
|
|
|
export type AzioneTipo = "attacco" | "ace" | "muro" | "errore" | "punto_avv" | "errore_avv";
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
export const azioniMeta: Record<
|
|
|
|
|
|
AzioneTipo,
|
|
|
|
|
|
{ label: string; short: string; nostro: boolean; richiedeGiocatore: boolean; className: string }
|
|
|
|
|
|
> = {
|
2026-09-01 13:38:33 +02:00
|
|
|
|
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",
|
|
|
|
|
|
},
|
2026-08-06 08:36:47 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type Azione = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
tipo: AzioneTipo;
|
|
|
|
|
|
giocatoreId?: string;
|
|
|
|
|
|
set: number;
|
|
|
|
|
|
ts: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-11-02 08:16:39 +01:00
|
|
|
|
/** 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 08:24:42 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 08:36:51 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 08:36:47 +02:00
|
|
|
|
export type ScoutMatch = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
data: string;
|
|
|
|
|
|
avversario: string;
|
|
|
|
|
|
casa: boolean;
|
|
|
|
|
|
setNostri: number;
|
|
|
|
|
|
setLoro: number;
|
|
|
|
|
|
parziali: Array<[number, number]>;
|
|
|
|
|
|
azioni: Azione[];
|
2026-11-02 08:16:39 +01:00
|
|
|
|
valutazioni: Valutazione[];
|
2026-08-06 08:36:47 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-01 16:08:59 +02:00
|
|
|
|
type RigaScoutPartita = {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
data: string;
|
|
|
|
|
|
avversario: string;
|
|
|
|
|
|
casa: boolean;
|
|
|
|
|
|
set_nostri: number;
|
|
|
|
|
|
set_loro: number;
|
|
|
|
|
|
parziali: unknown;
|
|
|
|
|
|
azioni: unknown;
|
2026-11-02 08:16:39 +01:00
|
|
|
|
valutazioni: unknown;
|
2026-09-01 16:08:59 +02:00
|
|
|
|
};
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
2026-09-01 16:08:59 +02:00
|
|
|
|
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) ?? [],
|
2026-11-02 08:16:39 +01:00
|
|
|
|
valutazioni: (r.valutazioni as Valutazione[] | null) ?? [],
|
2026-09-01 16:08:59 +02:00
|
|
|
|
};
|
2026-08-06 08:36:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 16:08:59 +02:00
|
|
|
|
export const SCOUT_MATCHES_KEY = ["scout-partite"] as const;
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
2026-09-01 16:08:59 +02:00
|
|
|
|
async function fetchScoutMatches(): Promise<ScoutMatch[]> {
|
|
|
|
|
|
const { data, error } = await supabase
|
|
|
|
|
|
.from("scout_partite")
|
2026-11-02 08:16:39 +01:00
|
|
|
|
.select("id, data, avversario, casa, set_nostri, set_loro, parziali, azioni, valutazioni")
|
2026-09-01 16:08:59 +02:00
|
|
|
|
.order("creato_il", { ascending: false });
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
return (data ?? []).map(daRiga);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 16:08:59 +02:00
|
|
|
|
/** Partite scoutate condivise con tutta la squadra: chi scoutizza le vede da qualsiasi
|
|
|
|
|
|
* dispositivo, non solo da quello di chi ha chiuso la partita. */
|
2026-08-06 08:36:47 +02:00
|
|
|
|
export function useScoutMatches(): ScoutMatch[] {
|
2026-09-01 16:08:59 +02:00
|
|
|
|
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)),
|
2026-11-02 08:16:39 +01:00
|
|
|
|
valutazioni: JSON.parse(JSON.stringify(input.match.valutazioni)),
|
2026-09-01 16:08:59 +02:00
|
|
|
|
});
|
|
|
|
|
|
if (error) throw error;
|
|
|
|
|
|
return input.match;
|
2026-08-06 08:36:47 +02:00
|
|
|
|
},
|
2026-09-06 19:30:49 +02:00
|
|
|
|
// 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),
|
|
|
|
|
|
]),
|
2026-09-01 16:08:59 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
},
|
2026-09-06 19:30:49 +02:00
|
|
|
|
onSuccess: (id) =>
|
|
|
|
|
|
queryClient.setQueryData<ScoutMatch[]>(SCOUT_MATCHES_KEY, (prec) =>
|
|
|
|
|
|
(prec ?? []).filter((m) => m.id !== id),
|
|
|
|
|
|
),
|
2026-09-01 16:08:59 +02:00
|
|
|
|
});
|
2026-08-06 08:36:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 08:36:51 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-06 08:36:47 +02:00
|
|
|
|
/** 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 22:42:35 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* MVP ricavati dalle partite scoutate (mappa matchId → nome vincitore).
|
|
|
|
|
|
* Le presenze personali restano su `risposte_presenze`, non sullo scout.
|
|
|
|
|
|
*/
|
2026-08-06 08:36:47 +02:00
|
|
|
|
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,
|
2026-09-01 22:42:35 +02:00
|
|
|
|
mvp,
|
2026-08-06 08:36:47 +02:00
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|