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
+95
-18
No files matched your search
@@ -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 }>();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
Reference in new issue
Block a user