2026-08-06 08:36:47 +02:00
|
|
|
|
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";
|
2026-11-02 08:24:42 +01:00
|
|
|
|
import { Select } from "@/components/crapp/ui-bits";
|
2026-09-03 10:17:03 +02:00
|
|
|
|
import { formatData } from "@/lib/crapp-data";
|
2026-08-06 08:36:47 +02:00
|
|
|
|
import type { Evento } from "@/lib/eventi";
|
2026-09-08 10:00:52 +02:00
|
|
|
|
import { useAnagraficaRosa } from "@/lib/rosa";
|
|
|
|
|
|
import { useGiocatoreBase } from "@/lib/user-store";
|
2026-08-06 08:36:47 +02:00
|
|
|
|
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,
|
2026-11-02 08:24:42 +01:00
|
|
|
|
contaPiuSenzaRicezione,
|
2026-11-02 08:16:39 +01:00
|
|
|
|
contaValutazioni,
|
2026-11-02 08:36:51 +01:00
|
|
|
|
parzialiFinali,
|
2026-11-02 08:16:39 +01:00
|
|
|
|
fondamentali,
|
2026-09-01 16:08:59 +02:00
|
|
|
|
useSalvaScoutMatch,
|
2026-08-06 08:36:47 +02:00
|
|
|
|
type Azione,
|
|
|
|
|
|
type AzioneTipo,
|
2026-11-02 08:16:39 +01:00
|
|
|
|
type Fondamentale,
|
|
|
|
|
|
type Valutazione,
|
2026-08-06 08:36:47 +02:00
|
|
|
|
} 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,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-01 13:38:33 +02:00
|
|
|
|
function Blocco({
|
|
|
|
|
|
icona,
|
|
|
|
|
|
titolo,
|
|
|
|
|
|
testo,
|
|
|
|
|
|
children,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
icona: React.ReactNode;
|
|
|
|
|
|
titolo: string;
|
|
|
|
|
|
testo: string;
|
|
|
|
|
|
children?: React.ReactNode;
|
|
|
|
|
|
}) {
|
2026-08-06 08:36:47 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="px-5 py-10">
|
|
|
|
|
|
<div className="rounded-3xl bg-card p-6 text-center shadow-card">
|
|
|
|
|
|
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-secondary text-accent">
|
|
|
|
|
|
{icona}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1 className="mt-4 font-display text-2xl uppercase leading-none">{titolo}</h1>
|
2026-10-06 11:50:28 +02:00
|
|
|
|
<p className="mt-2 text-[15px] text-muted-foreground">{testo}</p>
|
2026-08-06 08:36:47 +02:00
|
|
|
|
{children}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Scout() {
|
|
|
|
|
|
const { pronto, partita } = usePartitaDiOggi();
|
2026-09-08 10:00:52 +02:00
|
|
|
|
// Solo `.id`/`.nome` servono qui: `useGiocatoreBase` basta, niente statistiche.
|
|
|
|
|
|
const io = useGiocatoreBase();
|
2026-08-06 08:36:47 +02:00
|
|
|
|
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) {
|
2026-09-01 13:38:33 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />
|
|
|
|
|
|
);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!partita) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Blocco
|
|
|
|
|
|
icona={<CalendarX2 className="h-6 w-6" />}
|
|
|
|
|
|
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;
|
2026-11-02 08:36:51 +01:00
|
|
|
|
const ripresa =
|
|
|
|
|
|
!!statoSalvato.data &&
|
|
|
|
|
|
(statoSalvato.data.azioni.length > 0 ||
|
|
|
|
|
|
(statoSalvato.data.valutazioni?.length ?? 0) > 0 ||
|
|
|
|
|
|
statoSalvato.data.setChiusi.length > 0);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
if (!controllo) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Blocco
|
|
|
|
|
|
icona={occupataDaAltri ? <Lock className="h-6 w-6" /> : <Radio className="h-6 w-6" />}
|
|
|
|
|
|
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 ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => sessione.refetch()}
|
2026-10-06 11:50:28 +02:00
|
|
|
|
className="mt-5 w-full rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase"
|
2026-08-06 08:36:47 +02:00
|
|
|
|
>
|
|
|
|
|
|
Aggiorna
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={apri.isPending || !io}
|
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
|
if (!io) return;
|
|
|
|
|
|
const ok = await apri.mutateAsync({
|
|
|
|
|
|
eventoId: partita.id,
|
|
|
|
|
|
giocatoreId: io.id,
|
|
|
|
|
|
nome: io.nome,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (ok) setControllo(true);
|
|
|
|
|
|
else toast.error("Un altro compagno ha appena preso lo scout");
|
|
|
|
|
|
}}
|
2026-10-06 11:50:28 +02:00
|
|
|
|
className="mt-5 w-full rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
2026-08-06 08:36:47 +02:00
|
|
|
|
>
|
|
|
|
|
|
{ripresa ? "Riprendi lo scout" : "Prendi il controllo"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Blocco>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ScoutBoard
|
|
|
|
|
|
partita={partita}
|
|
|
|
|
|
iniziale={statoSalvato.data ?? null}
|
|
|
|
|
|
onFine={() => {
|
|
|
|
|
|
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(
|
2026-09-01 13:38:33 +02:00
|
|
|
|
partita.titolo
|
|
|
|
|
|
.replace(/CRAP Volley/gi, "")
|
|
|
|
|
|
.replace(/\s*vs\s*/i, "")
|
|
|
|
|
|
.trim() || "Avversario",
|
2026-08-06 08:36:47 +02:00
|
|
|
|
partita.titolo.trim().toLowerCase().startsWith("crap"),
|
|
|
|
|
|
);
|
2026-11-02 08:16:39 +01:00
|
|
|
|
const [avversario] = useState(base.avversario);
|
|
|
|
|
|
const [casa] = useState(base.casa);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
const [setChiusi, setSetChiusi] = useState<Array<[number, number]>>(base.setChiusi);
|
|
|
|
|
|
const [azioni, setAzioni] = useState<Azione[]>(base.azioni);
|
2026-11-02 08:16:39 +01:00
|
|
|
|
const [valutazioni, setValutazioni] = useState<Valutazione[]>(base.valutazioni ?? []);
|
2026-11-02 08:24:42 +01:00
|
|
|
|
const [ordine, setOrdine] = useState<"cognome" | "numero">("cognome");
|
2026-08-06 08:36:47 +02:00
|
|
|
|
const [selezionato, setSelezionato] = useState<string | null>(null);
|
|
|
|
|
|
const salva = useSalvaStatoScout();
|
|
|
|
|
|
const cancella = useCancellaStatoScout();
|
2026-09-01 16:08:59 +02:00
|
|
|
|
const salvaMatch = useSalvaScoutMatch();
|
2026-08-06 08:36:47 +02:00
|
|
|
|
const { risposte } = usePresenzeEvento(partita.id);
|
2026-09-08 10:00:52 +02:00
|
|
|
|
// Solo id/nome/numero servono (selezione e display): niente statistiche di squadra.
|
|
|
|
|
|
const rosa = useAnagraficaRosa();
|
2026-08-06 08:36:47 +02:00
|
|
|
|
const finito = useRef(false);
|
2026-11-02 08:36:51 +01:00
|
|
|
|
const codaSalvataggio = useRef<Promise<unknown>>(Promise.resolve());
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
/** In campo solo chi ha confermato la presenza (anche in ritardo). */
|
|
|
|
|
|
const convocati = useMemo(() => {
|
2026-09-03 10:17:03 +02:00
|
|
|
|
const presenti = rosa.filter(
|
2026-08-06 08:36:47 +02:00
|
|
|
|
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
|
|
|
|
|
|
);
|
2026-11-02 08:24:42 +01:00
|
|
|
|
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]);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
// Salvataggio condiviso: se chi scouta si disconnette, il prossimo riprende da qui.
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (finito.current) return;
|
2026-11-02 08:36:51 +01:00
|
|
|
|
// 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,
|
|
|
|
|
|
});
|
2026-08-06 08:36:47 +02:00
|
|
|
|
});
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2026-11-02 08:16:39 +01:00
|
|
|
|
}, [azioni, valutazioni, setChiusi, avversario, casa, partita.id]);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-11-02 08:36:51 +01:00
|
|
|
|
const setHaDati = azioniSet.length > 0 || valutazioni.some((v) => v.set === setCorrente);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
|
2026-11-02 08:16:39 +01:00
|
|
|
|
function valuta(fondamentale: Fondamentale, segno: Valutazione["segno"]) {
|
|
|
|
|
|
if (!selezionato) {
|
2026-08-06 08:36:47 +02:00
|
|
|
|
toast.error("Seleziona prima il giocatore");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-11-02 08:16:39 +01:00
|
|
|
|
setValutazioni((prev) => [
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
{
|
2026-08-06 08:36:47 +02:00
|
|
|
|
id: `${Date.now()}-${prev.length}`,
|
2026-11-02 08:16:39 +01:00
|
|
|
|
giocatoreId: selezionato,
|
|
|
|
|
|
fondamentale,
|
|
|
|
|
|
segno,
|
2026-08-06 08:36:47 +02:00
|
|
|
|
set: setCorrente,
|
|
|
|
|
|
ts: Date.now(),
|
2026-11-02 08:16:39 +01:00
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
2026-08-06 08:36:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-11-02 08:36:51 +01:00
|
|
|
|
if (!setHaDati) {
|
|
|
|
|
|
toast.error("Set senza valutazioni");
|
2026-08-06 08:36:47 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setSetChiusi((prev) => [...prev, [puntiNoi, puntiLoro]]);
|
2026-11-02 08:36:51 +01:00
|
|
|
|
toast.success(`Set ${setCorrente} chiuso`);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-01 16:08:59 +02:00
|
|
|
|
async function finePartita() {
|
2026-11-02 08:36:51 +01:00
|
|
|
|
const parziali = parzialiFinali(setChiusi, [puntiNoi, puntiLoro], setHaDati);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
if (parziali.length === 0) {
|
|
|
|
|
|
toast.error("Nessun set da salvare");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const vinti = parziali.filter(([n, l]) => n > l).length;
|
2026-09-01 16:08:59 +02:00
|
|
|
|
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,
|
2026-11-02 08:16:39 +01:00
|
|
|
|
valutazioni,
|
2026-09-01 16:08:59 +02:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
toast.error("Salvataggio non riuscito, riprova");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-06 08:36:47 +02:00
|
|
|
|
toast.success("Partita salvata: ora la squadra può votare l'MVP");
|
|
|
|
|
|
finito.current = true;
|
|
|
|
|
|
void cancella.mutateAsync(partita.id);
|
|
|
|
|
|
onFine();
|
|
|
|
|
|
navigate({ to: "/squadra" });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-02 08:16:39 +01:00
|
|
|
|
const giocatoreSel = rosa.find((g) => g.id === selezionato);
|
|
|
|
|
|
const contiSelezionato = selezionato
|
|
|
|
|
|
? contaValutazioni(valutazioni, selezionato, setCorrente)
|
|
|
|
|
|
: null;
|
2026-08-06 08:36:47 +02:00
|
|
|
|
const ultime = [...azioni].slice(-4).reverse();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<header className="sticky top-0 z-30 bg-hero px-5 pb-4 pt-6 text-primary-foreground">
|
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
2026-10-06 11:50:28 +02:00
|
|
|
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary-foreground/10 px-2.5 py-1 text-[13px] font-bold uppercase tracking-wide">
|
2026-08-06 08:36:47 +02:00
|
|
|
|
<Radio className="h-3 w-3 text-accent" /> Scout live · Set {setCorrente}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={annulla}
|
|
|
|
|
|
disabled={azioniSet.length === 0}
|
2026-10-06 11:50:28 +02:00
|
|
|
|
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-[13px] font-bold disabled:opacity-40"
|
2026-08-06 08:36:47 +02:00
|
|
|
|
>
|
|
|
|
|
|
<Undo2 className="h-3.5 w-3.5" /> Annulla
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onFine}
|
2026-10-06 11:50:28 +02:00
|
|
|
|
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-[13px] font-bold"
|
2026-08-06 08:36:47 +02:00
|
|
|
|
>
|
|
|
|
|
|
<LogOut className="h-3.5 w-3.5" /> Rilascia
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="px-5 pt-4">
|
2026-11-02 08:24:42 +01:00
|
|
|
|
<div className="mb-2 flex items-center justify-between gap-2">
|
|
|
|
|
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
|
Selezione giocatore
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="w-40 shrink-0">
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={ordine}
|
|
|
|
|
|
onChange={(e) => setOrdine(e.target.value as "cognome" | "numero")}
|
|
|
|
|
|
aria-label="Ordina i giocatori"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="cognome">Per cognome</option>
|
|
|
|
|
|
<option value="numero">Per numero</option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-06 08:36:47 +02:00
|
|
|
|
<div className="grid grid-cols-4 gap-2">
|
|
|
|
|
|
{convocati.map((g) => {
|
|
|
|
|
|
const attivo = selezionato === g.id;
|
2026-11-02 08:24:42 +01:00
|
|
|
|
const piu = contaPiuSenzaRicezione(valutazioni, g.id, setCorrente);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={g.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setSelezionato(attivo ? null : g.id)}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"rounded-2xl p-2 text-center transition-all active:scale-95",
|
|
|
|
|
|
attivo ? "bg-accent text-accent-foreground shadow-pop" : "bg-card shadow-card",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<p className="font-display text-2xl leading-none">{g.numero}</p>
|
2026-10-06 11:50:28 +02:00
|
|
|
|
<p className="mt-1 text-[13px] font-bold leading-tight">{g.nome}</p>
|
2026-08-06 08:36:47 +02:00
|
|
|
|
<p
|
|
|
|
|
|
className={cn(
|
2026-10-06 11:50:28 +02:00
|
|
|
|
"text-[13px] tabular-nums",
|
2026-08-06 08:36:47 +02:00
|
|
|
|
attivo ? "text-accent-foreground/70" : "text-muted-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-11-02 08:24:42 +01:00
|
|
|
|
+{piu}
|
2026-08-06 08:36:47 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="px-5 pt-4">
|
2026-11-02 08:16:39 +01:00
|
|
|
|
<div className="mb-2 flex items-center justify-between gap-2">
|
|
|
|
|
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
|
2. Valuta{giocatoreSel ? ` ${giocatoreSel.cognome} - ${giocatoreSel.numero}` : ""} · Set{" "}
|
|
|
|
|
|
{setCorrente}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={annullaValutazione}
|
|
|
|
|
|
disabled={!valutazioni.some((v) => v.set === setCorrente)}
|
|
|
|
|
|
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-secondary px-3 py-1.5 text-[13px] font-bold disabled:opacity-40"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Undo2 className="h-3.5 w-3.5" /> Annulla
|
|
|
|
|
|
</button>
|
2026-08-06 08:36:47 +02:00
|
|
|
|
</div>
|
2026-11-02 08:16:39 +01:00
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"space-y-2 rounded-3xl bg-card p-3 shadow-card",
|
|
|
|
|
|
!selezionato && "opacity-60",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{!selezionato ? (
|
|
|
|
|
|
<p className="text-[13px] text-muted-foreground">
|
|
|
|
|
|
Tocca un giocatore: i + e i − non cambiano il punteggio.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{fondamentali.map((f) => {
|
|
|
|
|
|
const c = contiSelezionato?.[f.id] ?? { piu: 0, meno: 0 };
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={f.id} className="grid grid-cols-[1fr_auto_auto] items-center gap-2">
|
|
|
|
|
|
<span className="text-[15px] font-bold">{f.label}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label={`${f.label}: meno`}
|
|
|
|
|
|
onClick={() => valuta(f.id, "-")}
|
|
|
|
|
|
className="min-w-16 rounded-2xl bg-destructive px-4 py-2.5 font-display text-xl leading-none text-destructive-foreground shadow-card transition-transform active:scale-95"
|
|
|
|
|
|
>
|
|
|
|
|
|
− <span className="tabular-nums">{c.meno}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label={`${f.label}: più`}
|
|
|
|
|
|
onClick={() => valuta(f.id, "+")}
|
|
|
|
|
|
className="min-w-16 rounded-2xl bg-success px-4 py-2.5 font-display text-xl leading-none text-success-foreground shadow-card transition-transform active:scale-95"
|
|
|
|
|
|
>
|
|
|
|
|
|
+ <span className="tabular-nums">{c.piu}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-08-06 08:36:47 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="px-5 pt-4">
|
|
|
|
|
|
<div className="rounded-3xl bg-card p-3 shadow-card">
|
2026-10-06 11:50:28 +02:00
|
|
|
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
2026-08-06 08:36:47 +02:00
|
|
|
|
Ultime azioni
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{ultime.length === 0 ? (
|
2026-10-06 11:50:28 +02:00
|
|
|
|
<p className="mt-2 text-[13px] text-muted-foreground">Nessuna azione registrata.</p>
|
2026-08-06 08:36:47 +02:00
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="mt-2 space-y-1.5">
|
|
|
|
|
|
{ultime.map((a, i) => {
|
2026-09-03 10:17:03 +02:00
|
|
|
|
const g = rosa.find((x) => x.id === a.giocatoreId);
|
2026-08-06 08:36:47 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<li
|
|
|
|
|
|
key={a.id}
|
|
|
|
|
|
className={cn(
|
2026-10-06 11:50:28 +02:00
|
|
|
|
"flex items-center justify-between gap-2 px-1 text-[13px]",
|
2026-08-06 08:36:47 +02:00
|
|
|
|
i === 0 && "anim-riga",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-09-01 13:38:33 +02:00
|
|
|
|
<span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span>
|
2026-08-06 08:36:47 +02:00
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
2026-10-06 11:50:28 +02:00
|
|
|
|
"shrink-0 rounded-full px-2 py-0.5 text-[13px] font-bold uppercase",
|
2026-08-06 08:36:47 +02:00
|
|
|
|
azioniMeta[a.tipo].className,
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{azioniMeta[a.tipo].short}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section className="grid grid-cols-2 gap-2 px-5 pb-4 pt-4">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={chiudiSet}
|
2026-10-06 11:50:28 +02:00
|
|
|
|
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase"
|
2026-08-06 08:36:47 +02:00
|
|
|
|
>
|
|
|
|
|
|
<CheckCircle2 className="h-4 w-4" /> Chiudi set
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={finePartita}
|
2026-09-01 16:08:59 +02:00
|
|
|
|
disabled={salvaMatch.isPending}
|
2026-10-06 11:50:28 +02:00
|
|
|
|
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
2026-08-06 08:36:47 +02:00
|
|
|
|
>
|
|
|
|
|
|
<Save className="h-4 w-4" /> Fine partita
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
2026-09-01 13:38:33 +02:00
|
|
|
|
}
|