Rilettura completa della documentazione confrontata con il codice. Dove la doc diceva il falso l'ho corretta; dove aveva ragione lei ho corretto il codice. Autovoto (la doc aveva ragione) - migration m12_niente_autovoto: vincoli mvp_no_autovoto e badge_social_no_autovoto, gli stessi che pagelle_voti ha dalla v1.0. Le righe che li violano vengono cancellate prima dell'ALTER, altrimenti fallisce; in locale non ce n'erano. M11 garantisce solo che il voto sia firmato con il proprio votante_id, non che il votato sia un altro: eleggersi MVP restava a un POST di distanza. - VotazioneMvp non mostra più il votante nell'elenco, come già faceva VotoSocial. Test che guardavano la colonna sbagliata - scritture.test.ts verificava che aggiornato_il si muovesse, chiamandolo "quello che alimenta la serie di conferme". È l'opposto: la serie usa risposto_il, che il trigger di M9 deve tenere fermo. Ora il test prova a riscriverlo e controlla che il database abbia tenuto la prima risposta; prima passava anche senza trigger. - destinatariSollecito() esce dalla route sollecita-presenze e diventa una funzione pura in presenze.ts, con i suoi test — stesso trattamento di avvisiPalloniEvento. Tre riletture in meno - giocatori-squadra, scout-store e avatar-store usavano invalidateQueries dove il dato scritto era già noto: ora setQueryData, come il resto dell'app. Resta scout-live, dove il lock può averlo vinto un altro dispositivo. Documentazione riallineata - presenze.md, badge.md, mvp.md: i limiti su RLS aperta e route non autenticata erano superati da M11 e DD-024; - serie-presenze.md: il filtro è e.data < oggi, non <=, e l'evento di oggi non conta (conterebbe come assenza per tutti); aggiunta la tabella risposto_il/aggiornato_il; - ARCHITECTURE.md ed EFFICIENZA_CLOUD.md: una sola eccezione a setQueryData; - DATABASE.md: i vincoli delle tre tabelle di voto; - PROJECT_STATE.md: fermo a M9, ora arriva a M12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
216 lines
5.8 KiB
TypeScript
216 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;
|
|
},
|
|
// 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),
|
|
),
|
|
});
|
|
}
|
|
|
|
/** 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,
|
|
};
|
|
});
|
|
}
|