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:
1 parent
74c78faac5
commit
d86b38d47b
6 files changed
+113
-36
No files matched your search
@@ -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 }>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
Reference in new issue
Block a user