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
+113 -36

No files matched your search

+21
View File
@@ -114,6 +114,18 @@ export function contaPiuSenzaRicezione(
).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;
@@ -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. */
export function totaliPerGiocatore(azioni: Azione[]) {
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 { PageHeader, TeamLogo } from "@/components/crapp/ui-bits";
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 {
isNostraSquadra,
@@ -150,7 +150,7 @@ function Classifica() {
const m = matchDaPartitaCsi(p);
return { ...m, mvp: mvpPerData(m.data), scout: false };
})
: scoutMatches.map((m) => ({
: scoutMatches.filter(haPunteggio).map((m) => ({
id: m.id,
data: m.data,
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 { useCsi } from "@/lib/csi";
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 { mediaVotoColpoDOcchio } from "@/lib/pagelle";
@@ -63,7 +63,9 @@ function Index() {
const votiMvp = useVotiMvp();
const mvpPerMatch = vincitoriMvp(votiMvp.data ?? []);
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 obiettivo = obiettivi.find((o) => progressoObiettivo(o) < 100) ?? obiettivi[0] ?? null;
const eventoUltima = ultimaBase
+21 -20
View File
@@ -11,7 +11,7 @@ import { DettaglioCsiEsteso, LogoSquadra, MetaPartitaCsi } from "@/components/cr
import { Pagelle } from "@/components/crapp/Pagelle";
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
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 { useIsAdmin } from "@/lib/ruoli";
import { useSonoAllenatore } from "@/lib/user-store";
@@ -70,25 +70,26 @@ function PartitaDetail() {
const csiMatch = csi
? partiteGiocate(csi.partite).find((p) => p.data === evento.data)
: undefined;
const match = scout
? {
id: scout.id,
data: scout.data,
avversario: scout.avversario,
logoAvversario: "",
casa: scout.casa,
setNostri: scout.setNostri,
setLoro: scout.setLoro,
parziali: scout.parziali,
campo: "",
girone: "",
numeroGara: "",
arbitro: "",
link: "",
}
: csiMatch
? matchDaPartitaCsi(csiMatch)
: undefined;
const match =
scout && haPunteggio(scout)
? {
id: scout.id,
data: scout.data,
avversario: scout.avversario,
logoAvversario: "",
casa: scout.casa,
setNostri: scout.setNostri,
setLoro: scout.setLoro,
parziali: scout.parziali,
campo: "",
girone: "",
numeroGara: "",
arbitro: "",
link: "",
}
: csiMatch
? matchDaPartitaCsi(csiMatch)
: undefined;
const totaliTeam = scout ? totaliSquadra([scout]) : null;
const avversario = evento.titolo.includes(" vs ")
? (evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo)
+26 -12
View File
@@ -29,6 +29,7 @@ import {
azioniMeta,
contaPiuSenzaRicezione,
contaValutazioni,
parzialiFinali,
fondamentali,
useSalvaScoutMatch,
type Azione,
@@ -121,7 +122,11 @@ function Scout() {
const attiva = sessione.data && !sessioneScaduta(sessione.data) ? sessione.data : null;
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) {
return (
@@ -212,6 +217,7 @@ function ScoutBoard({
// Solo id/nome/numero servono (selezione e display): niente statistiche di squadra.
const rosa = useAnagraficaRosa();
const finito = useRef(false);
const codaSalvataggio = useRef<Promise<unknown>>(Promise.resolve());
/** In campo solo chi ha confermato la presenza (anche in ritardo). */
const convocati = useMemo(() => {
@@ -229,13 +235,21 @@ function ScoutBoard({
// Salvataggio condiviso: se chi scouta si disconnette, il prossimo riprende da qui.
useEffect(() => {
if (finito.current) return;
const id = window.setTimeout(() => {
void salva.mutateAsync({
eventoId: partita.id,
stato: { azioni, valutazioni, setChiusi, avversario, casa },
// Subito a ogni tocco, non dopo una pausa: così non resta nulla in sospeso se l'app si
// chiude. Il salvataggio è sempre lo stato intero, quindi uno riuscito dopo uno fallito
// rimette a posto tutto; la coda li tiene in ordine.
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
}, [azioni, valutazioni, setChiusi, avversario, casa, partita.id]);
@@ -243,6 +257,7 @@ function ScoutBoard({
const azioniSet = azioni.filter((a) => a.set === setCorrente);
const puntiNoi = azioniSet.filter((a) => azioniMeta[a.tipo].nostro).length;
const puntiLoro = azioniSet.length - puntiNoi;
const setHaDati = azioniSet.length > 0 || valutazioni.some((v) => v.set === setCorrente);
function valuta(fondamentale: Fondamentale, segno: Valutazione["segno"]) {
if (!selezionato) {
@@ -281,17 +296,16 @@ function ScoutBoard({
}
function chiudiSet() {
if (puntiNoi === 0 && puntiLoro === 0) {
toast.error("Set senza azioni");
if (!setHaDati) {
toast.error("Set senza valutazioni");
return;
}
setSetChiusi((prev) => [...prev, [puntiNoi, puntiLoro]]);
toast.success(`Set ${setCorrente} chiuso ${puntiNoi}-${puntiLoro}`);
toast.success(`Set ${setCorrente} chiuso`);
}
async function finePartita() {
const parziali: Array<[number, number]> =
puntiNoi + puntiLoro > 0 ? [...setChiusi, [puntiNoi, puntiLoro]] : setChiusi;
const parziali = parzialiFinali(setChiusi, [puntiNoi, puntiLoro], setHaDati);
if (parziali.length === 0) {
toast.error("Nessun set da salvare");
return;
+39
View File
@@ -4,6 +4,8 @@ import { giocatori } from "@/lib/crapp-data";
import {
azioniMeta,
contaPiuSenzaRicezione,
haPunteggio,
parzialiFinali,
contaValutazioni,
fondamentali,
giocatoriConScout,
@@ -158,3 +160,40 @@ assert.equal(
);
assert.equal(contaPiuSenzaRicezione(piuVal, "g1", 2), 1);
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,
);