Scout live: salvataggio immediato e chiusura di set e partita senza punti

Lo stato in corso si salva a ogni tocco (in coda, con avviso se fallisce). «Chiudi set» e
«Fine partita» funzionano con le sole valutazioni; le partite senza punteggio (haPunteggio) non
compaiono come risultato in Home, Classifica e pagina partita. Il pulsante di ripresa riconosce
valutazioni e set chiusi.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-11-02 08:36:51 +01:00
1 parent 74c78faac5
commit d86b38d47b
6 files changed
+95 -18

No files matched your search

+21
View File
@@ -114,6 +114,18 @@ export function contaPiuSenzaRicezione(
).length; ).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 = { export type ScoutMatch = {
id: string; id: string;
data: string; data: string;
@@ -217,6 +229,15 @@ export function useEliminaScoutMatch() {
}); });
} }
/**
* 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. */ /** Somma delle azioni di un match per giocatore. */
export function totaliPerGiocatore(azioni: Azione[]) { export function totaliPerGiocatore(azioni: Azione[]) {
const out = new Map<string, { punti: number; ace: number; muri: number; errori: number }>(); const out = new Map<string, { punti: number; ace: number; muri: number; errori: number }>();
+2 -2
View File
@@ -5,7 +5,7 @@ import { cn } from "@/lib/utils";
import { formatData, type RigaClassifica } from "@/lib/crapp-data"; import { formatData, type RigaClassifica } from "@/lib/crapp-data";
import { PageHeader, TeamLogo } from "@/components/crapp/ui-bits"; import { PageHeader, TeamLogo } from "@/components/crapp/ui-bits";
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni"; import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
import { useScoutMatches } from "@/lib/scout-store"; import { haPunteggio, useScoutMatches } from "@/lib/scout-store";
import { useCsi } from "@/lib/csi"; import { useCsi } from "@/lib/csi";
import { import {
isNostraSquadra, isNostraSquadra,
@@ -150,7 +150,7 @@ function Classifica() {
const m = matchDaPartitaCsi(p); const m = matchDaPartitaCsi(p);
return { ...m, mvp: mvpPerData(m.data), scout: false }; return { ...m, mvp: mvpPerData(m.data), scout: false };
}) })
: scoutMatches.map((m) => ({ : scoutMatches.filter(haPunteggio).map((m) => ({
id: m.id, id: m.id,
data: m.data, data: m.data,
avversario: m.avversario, avversario: m.avversario,
+4 -2
View File
@@ -19,7 +19,7 @@ import { isAllenatore, nomeCompleto } from "@/lib/giocatori-squadra";
import { useGiocatoreBase } from "@/lib/user-store"; import { useGiocatoreBase } from "@/lib/user-store";
import { useCsi } from "@/lib/csi"; import { useCsi } from "@/lib/csi";
import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core"; import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
import { useScoutMatches } from "@/lib/scout-store"; import { haPunteggio, useScoutMatches } from "@/lib/scout-store";
import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti"; import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti";
import { mediaVotoColpoDOcchio } from "@/lib/pagelle"; import { mediaVotoColpoDOcchio } from "@/lib/pagelle";
@@ -63,7 +63,9 @@ function Index() {
const votiMvp = useVotiMvp(); const votiMvp = useVotiMvp();
const mvpPerMatch = vincitoriMvp(votiMvp.data ?? []); const mvpPerMatch = vincitoriMvp(votiMvp.data ?? []);
const csiGiocate = csi ? partiteGiocate(csi.partite) : []; const csiGiocate = csi ? partiteGiocate(csi.partite) : [];
const ultimaBase = csiGiocate[0] ? matchDaPartitaCsi(csiGiocate[0]) : (scoutMatches[0] ?? null); const ultimaBase = csiGiocate[0]
? matchDaPartitaCsi(csiGiocate[0])
: (scoutMatches.find(haPunteggio) ?? null);
const obiettivi = useObiettivi(); const obiettivi = useObiettivi();
const obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null; const obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null;
const eventoUltima = ultimaBase const eventoUltima = ultimaBase
+3 -2
View File
@@ -11,7 +11,7 @@ import { DettaglioCsiEsteso, LogoSquadra, MetaPartitaCsi } from "@/components/cr
import { Pagelle } from "@/components/crapp/Pagelle"; import { Pagelle } from "@/components/crapp/Pagelle";
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche"; import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
import { ScoutEntry } from "@/components/crapp/ScoutEntry"; import { ScoutEntry } from "@/components/crapp/ScoutEntry";
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store"; import { haPunteggio, useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export"; import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
import { useIsAdmin } from "@/lib/ruoli"; import { useIsAdmin } from "@/lib/ruoli";
import { useSonoAllenatore } from "@/lib/user-store"; import { useSonoAllenatore } from "@/lib/user-store";
@@ -70,7 +70,8 @@ function PartitaDetail() {
const csiMatch = csi const csiMatch = csi
? partiteGiocate(csi.partite).find((p) => p.data === evento.data) ? partiteGiocate(csi.partite).find((p) => p.data === evento.data)
: undefined; : undefined;
const match = scout const match =
scout && haPunteggio(scout)
? { ? {
id: scout.id, id: scout.id,
data: scout.data, data: scout.data,
+26 -12
View File
@@ -29,6 +29,7 @@ import {
azioniMeta, azioniMeta,
contaPiuSenzaRicezione, contaPiuSenzaRicezione,
contaValutazioni, contaValutazioni,
parzialiFinali,
fondamentali, fondamentali,
useSalvaScoutMatch, useSalvaScoutMatch,
type Azione, type Azione,
@@ -121,7 +122,11 @@ function Scout() {
const attiva = sessione.data && !sessioneScaduta(sessione.data) ? sessione.data : null; const attiva = sessione.data && !sessioneScaduta(sessione.data) ? sessione.data : null;
const occupataDaAltri = !!attiva && attiva.giocatore_id !== io?.id; const occupataDaAltri = !!attiva && attiva.giocatore_id !== io?.id;
const ripresa = !!statoSalvato.data && statoSalvato.data.azioni.length > 0; const ripresa =
!!statoSalvato.data &&
(statoSalvato.data.azioni.length > 0 ||
(statoSalvato.data.valutazioni?.length ?? 0) > 0 ||
statoSalvato.data.setChiusi.length > 0);
if (!controllo) { if (!controllo) {
return ( return (
@@ -212,6 +217,7 @@ function ScoutBoard({
// Solo id/nome/numero servono (selezione e display): niente statistiche di squadra. // Solo id/nome/numero servono (selezione e display): niente statistiche di squadra.
const rosa = useAnagraficaRosa(); const rosa = useAnagraficaRosa();
const finito = useRef(false); const finito = useRef(false);
const codaSalvataggio = useRef<Promise<unknown>>(Promise.resolve());
/** In campo solo chi ha confermato la presenza (anche in ritardo). */ /** In campo solo chi ha confermato la presenza (anche in ritardo). */
const convocati = useMemo(() => { const convocati = useMemo(() => {
@@ -229,13 +235,21 @@ function ScoutBoard({
// Salvataggio condiviso: se chi scouta si disconnette, il prossimo riprende da qui. // Salvataggio condiviso: se chi scouta si disconnette, il prossimo riprende da qui.
useEffect(() => { useEffect(() => {
if (finito.current) return; if (finito.current) return;
const id = window.setTimeout(() => { // Subito a ogni tocco, non dopo una pausa: così non resta nulla in sospeso se l'app si
void salva.mutateAsync({ // chiude. Il salvataggio è sempre lo stato intero, quindi uno riuscito dopo uno fallito
eventoId: partita.id, // rimette a posto tutto; la coda li tiene in ordine.
stato: { azioni, valutazioni, setChiusi, avversario, casa }, const stato = { azioni, valutazioni, setChiusi, avversario, casa };
codaSalvataggio.current = codaSalvataggio.current
.then(() => salva.mutateAsync({ eventoId: partita.id, stato }))
.then(() => {
toast.dismiss("scout-salvataggio");
})
.catch(() => {
toast.error("Salvataggio non riuscito: controlla la connessione", {
id: "scout-salvataggio",
duration: Infinity,
});
}); });
}, 800);
return () => window.clearTimeout(id);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [azioni, valutazioni, setChiusi, avversario, casa, partita.id]); }, [azioni, valutazioni, setChiusi, avversario, casa, partita.id]);
@@ -243,6 +257,7 @@ function ScoutBoard({
const azioniSet = azioni.filter((a) => a.set === setCorrente); const azioniSet = azioni.filter((a) => a.set === setCorrente);
const puntiNoi = azioniSet.filter((a) => azioniMeta[a.tipo].nostro).length; const puntiNoi = azioniSet.filter((a) => azioniMeta[a.tipo].nostro).length;
const puntiLoro = azioniSet.length - puntiNoi; const puntiLoro = azioniSet.length - puntiNoi;
const setHaDati = azioniSet.length > 0 || valutazioni.some((v) => v.set === setCorrente);
function valuta(fondamentale: Fondamentale, segno: Valutazione["segno"]) { function valuta(fondamentale: Fondamentale, segno: Valutazione["segno"]) {
if (!selezionato) { if (!selezionato) {
@@ -281,17 +296,16 @@ function ScoutBoard({
} }
function chiudiSet() { function chiudiSet() {
if (puntiNoi === 0 && puntiLoro === 0) { if (!setHaDati) {
toast.error("Set senza azioni"); toast.error("Set senza valutazioni");
return; return;
} }
setSetChiusi((prev) => [...prev, [puntiNoi, puntiLoro]]); setSetChiusi((prev) => [...prev, [puntiNoi, puntiLoro]]);
toast.success(`Set ${setCorrente} chiuso ${puntiNoi}-${puntiLoro}`); toast.success(`Set ${setCorrente} chiuso`);
} }
async function finePartita() { async function finePartita() {
const parziali: Array<[number, number]> = const parziali = parzialiFinali(setChiusi, [puntiNoi, puntiLoro], setHaDati);
puntiNoi + puntiLoro > 0 ? [...setChiusi, [puntiNoi, puntiLoro]] : setChiusi;
if (parziali.length === 0) { if (parziali.length === 0) {
toast.error("Nessun set da salvare"); toast.error("Nessun set da salvare");
return; return;
+39
View File
@@ -4,6 +4,8 @@ import { giocatori } from "@/lib/crapp-data";
import { import {
azioniMeta, azioniMeta,
contaPiuSenzaRicezione, contaPiuSenzaRicezione,
haPunteggio,
parzialiFinali,
contaValutazioni, contaValutazioni,
fondamentali, fondamentali,
giocatoriConScout, giocatoriConScout,
@@ -158,3 +160,40 @@ assert.equal(
); );
assert.equal(contaPiuSenzaRicezione(piuVal, "g1", 2), 1); assert.equal(contaPiuSenzaRicezione(piuVal, "g1", 2), 1);
assert.equal(contaPiuSenzaRicezione(piuVal, "g3", 1), 0); assert.equal(contaPiuSenzaRicezione(piuVal, "g3", 1), 0);
// --- parziali finali: il set in corso conta solo se ha dati ------------------------
assert.deepEqual(
parzialiFinali([[25, 20]], [0, 0], true),
[
[25, 20],
[0, 0],
],
"set con sole valutazioni",
);
assert.deepEqual(
parzialiFinali([[25, 20]], [0, 0], false),
[[25, 20]],
"set vuoto: non si archivia",
);
assert.deepEqual(parzialiFinali([], [0, 0], false), [], "nessun set da salvare");
// --- haPunteggio: le partite di sole valutazioni non sono un risultato -------------
assert.equal(
haPunteggio({
parziali: [
[0, 0],
[0, 0],
],
}),
false,
);
assert.equal(haPunteggio({ parziali: [] }), false);
assert.equal(
haPunteggio({
parziali: [
[0, 0],
[25, 18],
],
}),
true,
);