diff --git a/src/lib/scout-store.ts b/src/lib/scout-store.ts index ffc0a9d..3eb5114 100644 --- a/src/lib/scout-store.ts +++ b/src/lib/scout-store.ts @@ -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): 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(); diff --git a/src/routes/classifica.tsx b/src/routes/classifica.tsx index a06abed..5e658e2 100644 --- a/src/routes/classifica.tsx +++ b/src/routes/classifica.tsx @@ -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, diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 1838ff0..66b05ed 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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 diff --git a/src/routes/partita.$id.tsx b/src/routes/partita.$id.tsx index 64dcdf0..7b84561 100644 --- a/src/routes/partita.$id.tsx +++ b/src/routes/partita.$id.tsx @@ -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) diff --git a/src/routes/scout.tsx b/src/routes/scout.tsx index d32a464..33420a4 100644 --- a/src/routes/scout.tsx +++ b/src/routes/scout.tsx @@ -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.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; diff --git a/test/unit/scout-store.test.ts b/test/unit/scout-store.test.ts index e0a0dd5..018a694 100644 --- a/test/unit/scout-store.test.ts +++ b/test/unit/scout-store.test.ts @@ -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, +);