import { useEffect, useMemo, useRef, useState } from "react"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { Undo2, Save, CheckCircle2, Radio, Lock, CalendarX2, LogOut } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Select } from "@/components/crapp/ui-bits"; import { formatData } from "@/lib/crapp-data"; import type { Evento } from "@/lib/eventi"; import { useAnagraficaRosa } from "@/lib/rosa"; import { useGiocatoreBase } from "@/lib/user-store"; import { usePresenzeEvento } from "@/lib/presenze"; import { statoIniziale, useCancellaStatoScout, useSalvaStatoScout, useStatoScout, type StatoScout, } from "@/lib/scout-stato"; import { SCADENZA_MINUTI, sessioneScaduta, useApriSessioneScout, useChiudiSessioneScout, useHeartbeatScout, usePartitaDiOggi, useSessioneScout, } from "@/lib/scout-live"; import { azioniMeta, contaPiuSenzaRicezione, contaValutazioni, parzialiFinali, fondamentali, useSalvaScoutMatch, type Azione, type AzioneTipo, type Fondamentale, type Valutazione, } from "@/lib/scout-store"; export const Route = createFileRoute("/scout")({ head: () => ({ meta: [ { title: "Scout live partita — CrAPP" }, { name: "description", content: "Registra punti, ace, muri ed errori del CRAP Volley in tempo reale durante la partita.", }, { property: "og:title", content: "Scout live partita — CrAPP" }, { property: "og:description", content: "Un tap per giocatore e azione: le stats finiscono subito nel database squadra.", }, ], }), component: Scout, }); function Blocco({ icona, titolo, testo, children, }: { icona: React.ReactNode; titolo: string; testo: string; children?: React.ReactNode; }) { return (
{icona}

{titolo}

{testo}

{children}
); } function Scout() { const { pronto, partita } = usePartitaDiOggi(); // Solo `.id`/`.nome` servono qui: `useGiocatoreBase` basta, niente statistiche. const io = useGiocatoreBase(); const sessione = useSessioneScout(partita?.id ?? null); const statoSalvato = useStatoScout(partita?.id ?? null); const apri = useApriSessioneScout(); const chiudi = useChiudiSessioneScout(); const [controllo, setControllo] = useState(false); useHeartbeatScout(partita?.id ?? null, io?.id ?? null, controllo); useEffect(() => { if (!controllo || !partita || !io) return; const rilascia = () => { void chiudi.mutateAsync({ eventoId: partita.id, giocatoreId: io.id }); }; window.addEventListener("pagehide", rilascia); return () => window.removeEventListener("pagehide", rilascia); // eslint-disable-next-line react-hooks/exhaustive-deps }, [controllo, partita?.id, io?.id]); if (!pronto || sessione.isLoading || statoSalvato.isLoading) { return ( } titolo="Scout live" testo="Caricamento…" /> ); } if (!partita) { return ( } titolo="Nessuna partita oggi" testo="Lo scout live si attiva automaticamente il giorno della partita." /> ); } 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 || (statoSalvato.data.valutazioni?.length ?? 0) > 0 || statoSalvato.data.setChiusi.length > 0); if (!controllo) { return ( : } titolo={occupataDaAltri ? "Scout occupato" : "Scout live disponibile"} testo={ occupataDaAltri ? `${attiva!.giocatore_nome} sta già scoutando questa partita. Si libera dopo ${SCADENZA_MINUTI} minuti di inattività.` : ripresa ? `${partita.titolo} · scout già avviato: riprendi da dove è stato lasciato.` : `${partita.titolo} · ${formatData(partita.data)} ore ${partita.ora}` } > {occupataDaAltri ? ( ) : ( )} ); } return ( { setControllo(false); if (io) void chiudi.mutateAsync({ eventoId: partita.id, giocatoreId: io.id }); }} /> ); } function ScoutBoard({ partita, iniziale, onFine, }: { partita: Evento; iniziale: StatoScout | null; onFine: () => void; }) { const navigate = useNavigate(); const base = iniziale ?? statoIniziale( partita.titolo .replace(/CRAP Volley/gi, "") .replace(/\s*vs\s*/i, "") .trim() || "Avversario", partita.titolo.trim().toLowerCase().startsWith("crap"), ); const [avversario] = useState(base.avversario); const [casa] = useState(base.casa); const [setChiusi, setSetChiusi] = useState>(base.setChiusi); const [azioni, setAzioni] = useState(base.azioni); const [valutazioni, setValutazioni] = useState(base.valutazioni ?? []); const [ordine, setOrdine] = useState<"cognome" | "numero">("cognome"); const [selezionato, setSelezionato] = useState(null); const salva = useSalvaStatoScout(); const cancella = useCancellaStatoScout(); const salvaMatch = useSalvaScoutMatch(); const { risposte } = usePresenzeEvento(partita.id); // 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(() => { const presenti = rosa.filter( (g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo", ); const elenco = presenti.length > 0 ? presenti : rosa; return [...elenco].sort((a, b) => ordine === "numero" ? a.numero - b.numero : a.cognome.localeCompare(b.cognome, "it") || a.numero - b.numero, ); }, [risposte, rosa, ordine]); // Salvataggio condiviso: se chi scouta si disconnette, il prossimo riprende da qui. useEffect(() => { if (finito.current) return; // 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, }); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [azioni, valutazioni, setChiusi, avversario, casa, partita.id]); const setCorrente = setChiusi.length + 1; 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) { toast.error("Seleziona prima il giocatore"); return; } setValutazioni((prev) => [ ...prev, { id: `${Date.now()}-${prev.length}`, giocatoreId: selezionato, fondamentale, segno, set: setCorrente, ts: Date.now(), }, ]); } function annullaValutazione() { const ultima = valutazioni[valutazioni.length - 1]; if (!ultima || ultima.set !== setCorrente) { toast.error("Puoi annullare solo le valutazioni del set in corso"); return; } setValutazioni((prev) => prev.slice(0, -1)); } function annulla() { const ultima = azioni[azioni.length - 1]; if (!ultima || ultima.set !== setCorrente) { toast.error("Puoi annullare solo le azioni del set in corso"); return; } setAzioni((prev) => prev.slice(0, -1)); } function chiudiSet() { if (!setHaDati) { toast.error("Set senza valutazioni"); return; } setSetChiusi((prev) => [...prev, [puntiNoi, puntiLoro]]); toast.success(`Set ${setCorrente} chiuso`); } async function finePartita() { const parziali = parzialiFinali(setChiusi, [puntiNoi, puntiLoro], setHaDati); if (parziali.length === 0) { toast.error("Nessun set da salvare"); return; } const vinti = parziali.filter(([n, l]) => n > l).length; try { await salvaMatch.mutateAsync({ eventoId: partita.id, match: { id: `s${Date.now()}`, data: new Date().toISOString().slice(0, 10), avversario: avversario.trim() || "Avversario", casa, setNostri: vinti, setLoro: parziali.length - vinti, parziali, azioni, valutazioni, }, }); } catch { toast.error("Salvataggio non riuscito, riprova"); return; } toast.success("Partita salvata"); finito.current = true; void cancella.mutateAsync(partita.id); onFine(); navigate({ to: "/squadra" }); } const giocatoreSel = rosa.find((g) => g.id === selezionato); const contiSelezionato = selezionato ? contaValutazioni(valutazioni, selezionato, setCorrente) : null; return ( <>
Scout live · Set {setCorrente}

Selezione giocatore

{convocati.map((g) => { const attivo = selezionato === g.id; const piu = contaPiuSenzaRicezione(valutazioni, g.id, setCorrente); return ( ); })}

Valuta{giocatoreSel ? ` ${giocatoreSel.cognome} - ${giocatoreSel.numero}` : ""} · Set{" "} {setCorrente}

{!selezionato ? (

Tocca un giocatore: i + e i − non cambiano il punteggio.

) : null} {fondamentali.map((f) => { const c = contiSelezionato?.[f.id] ?? { piu: 0, meno: 0 }; return (
{f.label}
); })}
); }