Due bug architetturali, entrambi con la stessa causa: dati che vivevano solo in localStorage, quindi visibili a un solo dispositivo. - Il blocco "chi sta scoutando" (scout-live.ts) non funzionava mai tra telefoni diversi: due persone potevano prendere il controllo insieme da dispositivi diversi, sovrascrivendosi a vicenda le azioni nel salvataggio condiviso. Ora usa scout_sessioni, tabella già presente nello schema ma mai collegata al codice. - La partita scoutata finita (scout-store.ts) veniva salvata solo in localStorage: "Punti/Ace/Muri squadra" e le presenze derivate dallo scout esistevano solo sul telefono di chi aveva chiuso la partita. Nuova tabella scout_partite archivia la partita completa (azioni incluse) per tutta la squadra. useScoutMatches() mantiene la stessa firma di prima (ScoutMatch[]), ora alimentata da una query invece che da localStorage: nessuna modifica necessaria nei punti che la leggono (squadra, classifica, home, dettaglio partita, rosa). Rigenerati i tipi Supabase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
210 lines
5.8 KiB
TypeScript
210 lines
5.8 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;
|
|
};
|
|
|
|
export type ScoutMatch = {
|
|
id: string;
|
|
data: string;
|
|
avversario: string;
|
|
casa: boolean;
|
|
setNostri: number;
|
|
setLoro: number;
|
|
parziali: Array<[number, number]>;
|
|
azioni: Azione[];
|
|
};
|
|
|
|
type RigaScoutPartita = {
|
|
id: string;
|
|
data: string;
|
|
avversario: string;
|
|
casa: boolean;
|
|
set_nostri: number;
|
|
set_loro: number;
|
|
parziali: unknown;
|
|
azioni: 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) ?? [],
|
|
};
|
|
}
|
|
|
|
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")
|
|
.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)),
|
|
});
|
|
if (error) throw error;
|
|
return input.match;
|
|
},
|
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: SCOUT_MATCHES_KEY }),
|
|
});
|
|
}
|
|
|
|
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: () => queryClient.invalidateQueries({ queryKey: SCOUT_MATCHES_KEY }),
|
|
});
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
/** Presenze e MVP ricavati dalle partite scoutate: nessuna statistica individuale offensiva.
|
|
* `mvpPerMatch` mappa idPartita -> nome dell'MVP eletto dalla squadra. */
|
|
export function giocatoriConScout(
|
|
matches: ScoutMatch[],
|
|
mvpPerMatch: Record<string, string> = {},
|
|
): Giocatore[] {
|
|
return giocatori.map((g) => {
|
|
let presenze = 0;
|
|
let mvp = 0;
|
|
for (const m of matches) {
|
|
if (totaliPerGiocatore(m.azioni).has(g.id)) presenze += 1;
|
|
if (mvpPerMatch[m.id] === g.nome) mvp += 1;
|
|
}
|
|
return {
|
|
...g,
|
|
mvp: g.mvp + mvp,
|
|
presenze: g.presenze + presenze,
|
|
totaliEventi: g.totaliEventi + matches.length,
|
|
};
|
|
});
|
|
}
|