Apre lo Scout Live dalla pagina partita e a tutta la squadra.
Tolto dalla home e poi da Squadra, `ScoutEntry` era rimasto orfano: `/scout` si raggiungeva solo scrivendo l'URL a mano. Ora la card sta nella sezione «Scout live» di `/partita/$id`. La prop `eventoId` la accende solo se la partita aperta è quella di oggi: senza, da una partita futura o passata si sarebbe finiti sullo scout di un'altra. Cade anche la riserva agli admin, in `ScoutEntry` e nella route: può scoutare chiunque sia autenticato, uno per volta grazie al lock di sessione. È anche l'unico controllo che c'era, visto che le policy RLS sono sempre state aperte a tutti gli autenticati. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,15 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
|
|||||||
|
|
||||||
## Versione attuale — agosto 2026
|
## Versione attuale — agosto 2026
|
||||||
|
|
||||||
|
### Lo Scout Live si apre dalla pagina della partita
|
||||||
|
|
||||||
|
- Tolto dalla home, era rimasto senza nessun link: `/scout` si raggiungeva solo scrivendo
|
||||||
|
l'URL. Ora la card `ScoutEntry` sta in `/partita/$id`, sezione «Scout live».
|
||||||
|
- Si accende solo se la partita aperta è quella di oggi (nuova prop `eventoId`), altrimenti
|
||||||
|
resta grigia con «Si attiva il giorno della partita». Lock di sessione invariato.
|
||||||
|
- Non è più riservato agli admin: può scoutare chiunque sia autenticato, uno per volta grazie
|
||||||
|
al lock. Sparisce il messaggio «Scout riservato».
|
||||||
|
|
||||||
### Il sondaggio pre-partita apre alle 8:00 del giorno della partita
|
### Il sondaggio pre-partita apre alle 8:00 del giorno della partita
|
||||||
|
|
||||||
- Prima era sempre votabile, anche settimane prima: ora la card resta chiusa con l'avviso di
|
- Prima era sempre votabile, anche settimane prima: ora la card resta chiusa con l'avviso di
|
||||||
|
|||||||
@@ -30,10 +30,21 @@ aggiornato da qualunque dispositivo.
|
|||||||
|
|
||||||
## Chi può usarlo
|
## Chi può usarlo
|
||||||
|
|
||||||
Solo gli admin lato UI: `ScoutEntry.tsx` e `scout.tsx` bloccano i non-admin con il messaggio
|
Chiunque sia autenticato: non è più riservato agli admin. A tenere l'ordine basta il lock di
|
||||||
"Scout riservato". **Il controllo non è imposto a livello database**: le policy RLS di
|
sessione — scoutizza uno per volta, gli altri vedono "In uso da …". Questo allinea l'interfaccia
|
||||||
`scout_sessioni`/`scout_live`/`scout_partite` sono aperte a qualunque utente autenticato, non
|
alle policy RLS di `scout_sessioni`/`scout_live`/`scout_partite`, che sono sempre state aperte a
|
||||||
solo agli admin — la migration M4 toglie l'accesso solo al ruolo `anon`.
|
qualunque utente autenticato (la migration M4 toglie l'accesso solo al ruolo `anon`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Da dove ci si arriva
|
||||||
|
|
||||||
|
`ScoutEntry.tsx` è l'unico accesso a `/scout`: sta nella pagina della partita
|
||||||
|
(`partita.$id.tsx`, sezione «Scout live»), visibile a tutta la squadra. Si accende solo se
|
||||||
|
**quella** partita è quella di oggi — la prop `eventoId` confronta l'evento aperto con
|
||||||
|
`partitaDiOggi()` — e se nessun altro ha il lock; negli altri casi resta una card grigia non
|
||||||
|
cliccabile («Si attiva il giorno della partita» / «In uso da …»). Dalla home è stato tolto
|
||||||
|
perché occupava spazio 6 giorni su 7.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -104,7 +115,8 @@ calcolati a runtime).
|
|||||||
|
|
||||||
## Limiti noti
|
## Limiti noti
|
||||||
|
|
||||||
- Controllo "solo admin" non imposto dal database (vedi sopra).
|
- Scout aperto a tutta la squadra: nessun filtro su chi può registrare le azioni, l'unica
|
||||||
|
garanzia è il lock di sessione (vedi sopra).
|
||||||
- Possibile, per quanto improbabile, doppio "successo" applicativo nel prendere il lock:
|
- Possibile, per quanto improbabile, doppio "successo" applicativo nel prendere il lock:
|
||||||
lettura e upsert non sono atomici.
|
lettura e upsert non sono atomici.
|
||||||
- `scout_partite` si inserisce ma non si corregge dall'interfaccia: solo eliminazione totale.
|
- `scout_partite` si inserisce ma non si corregge dall'interfaccia: solo eliminazione totale.
|
||||||
@@ -116,4 +128,3 @@ calcolati a runtime).
|
|||||||
## Evoluzioni possibili
|
## Evoluzioni possibili
|
||||||
|
|
||||||
- Realtime (Supabase Realtime) per aggiornare la sessione condivisa senza refresh manuale.
|
- Realtime (Supabase Realtime) per aggiornare la sessione condivisa senza refresh manuale.
|
||||||
- Restringere le policy RLS al solo ruolo admin.
|
|
||||||
|
|||||||
@@ -3,32 +3,29 @@ import { ChevronRight, Lock, Radio } from "lucide-react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { sessioneScaduta, usePartitaDiOggi, useSessioneScout } from "@/lib/scout-live";
|
import { sessioneScaduta, usePartitaDiOggi, useSessioneScout } from "@/lib/scout-live";
|
||||||
import { useGiocatoreCorrente } from "@/lib/user-store";
|
import { useGiocatoreCorrente } from "@/lib/user-store";
|
||||||
import { useIsAdmin } from "@/lib/ruoli";
|
|
||||||
|
|
||||||
/** Accesso allo scout live: attivo solo il giorno della partita e se nessun altro lo sta usando. */
|
/**
|
||||||
export function ScoutEntry({ variante = "grande" }: { variante?: "grande" | "compatto" }) {
|
* Accesso allo scout live: attivo solo il giorno della partita e se nessun altro lo sta usando.
|
||||||
const { pronto, partita } = usePartitaDiOggi();
|
* Con `eventoId` si accende solo se quella partita è proprio quella di oggi.
|
||||||
|
*/
|
||||||
|
export function ScoutEntry({
|
||||||
|
variante = "grande",
|
||||||
|
eventoId,
|
||||||
|
}: {
|
||||||
|
variante?: "grande" | "compatto";
|
||||||
|
eventoId?: string;
|
||||||
|
}) {
|
||||||
|
const { pronto, partita: diOggi } = usePartitaDiOggi();
|
||||||
|
const partita = eventoId && diOggi?.id !== eventoId ? null : diOggi;
|
||||||
const io = useGiocatoreCorrente();
|
const io = useGiocatoreCorrente();
|
||||||
const admin = useIsAdmin();
|
|
||||||
const { data: sessione } = useSessioneScout(partita?.id ?? null);
|
const { data: sessione } = useSessioneScout(partita?.id ?? null);
|
||||||
|
|
||||||
// Strumento tecnico: solo i referenti/allenatori scoutizzano la partita.
|
|
||||||
const abilitato = admin;
|
|
||||||
|
|
||||||
const attiva = sessione && !sessioneScaduta(sessione) ? sessione : null;
|
const attiva = sessione && !sessioneScaduta(sessione) ? sessione : null;
|
||||||
const occupato = !!attiva && attiva.giocatore_id !== io?.id;
|
const occupato = !!attiva && attiva.giocatore_id !== io?.id;
|
||||||
const disponibile = abilitato && pronto && !!partita && !occupato;
|
const disponibile = pronto && !!partita && !occupato;
|
||||||
|
|
||||||
const titolo = !abilitato
|
const titolo = !partita ? "Scout live non attivo" : occupato ? "Scout occupato" : "Scout live";
|
||||||
? "Scout live"
|
const sottotitolo = !partita
|
||||||
: !partita
|
|
||||||
? "Scout live non attivo"
|
|
||||||
: occupato
|
|
||||||
? "Scout occupato"
|
|
||||||
: "Scout live";
|
|
||||||
const sottotitolo = !abilitato
|
|
||||||
? "Riservato ad allenatori e referenti"
|
|
||||||
: !partita
|
|
||||||
? "Si attiva il giorno della partita"
|
? "Si attiva il giorno della partita"
|
||||||
: occupato
|
: occupato
|
||||||
? `In uso da ${attiva!.giocatore_nome}`
|
? `In uso da ${attiva!.giocatore_nome}`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useCsi } from "@/lib/csi";
|
|||||||
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
||||||
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 { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
|
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
|
||||||
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
|
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
|
||||||
import { useGiocatoreCorrente } from "@/lib/user-store";
|
import { useGiocatoreCorrente } from "@/lib/user-store";
|
||||||
@@ -204,6 +205,10 @@ function PartitaDetail() {
|
|||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Section titolo="Scout live">
|
||||||
|
<ScoutEntry eventoId={evento.id} />
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section titolo="Sondaggio pre-partita">
|
<Section titolo="Sondaggio pre-partita">
|
||||||
<SondaggioCacche eventoId={evento.id} dataEvento={evento.data} />
|
<SondaggioCacche eventoId={evento.id} dataEvento={evento.data} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { formatData } from "@/lib/crapp-data";
|
|||||||
import type { Evento } from "@/lib/eventi";
|
import type { Evento } from "@/lib/eventi";
|
||||||
import { useRosa } from "@/lib/rosa";
|
import { useRosa } from "@/lib/rosa";
|
||||||
import { useGiocatoreCorrente } from "@/lib/user-store";
|
import { useGiocatoreCorrente } from "@/lib/user-store";
|
||||||
import { useIsAdmin } from "@/lib/ruoli";
|
|
||||||
import { usePresenzeEvento } from "@/lib/presenze";
|
import { usePresenzeEvento } from "@/lib/presenze";
|
||||||
import {
|
import {
|
||||||
statoIniziale,
|
statoIniziale,
|
||||||
@@ -83,7 +82,6 @@ function Blocco({
|
|||||||
function Scout() {
|
function Scout() {
|
||||||
const { pronto, partita } = usePartitaDiOggi();
|
const { pronto, partita } = usePartitaDiOggi();
|
||||||
const io = useGiocatoreCorrente();
|
const io = useGiocatoreCorrente();
|
||||||
const admin = useIsAdmin();
|
|
||||||
const sessione = useSessioneScout(partita?.id ?? null);
|
const sessione = useSessioneScout(partita?.id ?? null);
|
||||||
const statoSalvato = useStatoScout(partita?.id ?? null);
|
const statoSalvato = useStatoScout(partita?.id ?? null);
|
||||||
const apri = useApriSessioneScout();
|
const apri = useApriSessioneScout();
|
||||||
@@ -102,16 +100,6 @@ function Scout() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [controllo, partita?.id, io?.id]);
|
}, [controllo, partita?.id, io?.id]);
|
||||||
|
|
||||||
if (io && !admin) {
|
|
||||||
return (
|
|
||||||
<Blocco
|
|
||||||
icona={<Lock className="h-6 w-6" />}
|
|
||||||
titolo="Scout riservato"
|
|
||||||
testo="Lo scout live è uno strumento tecnico per allenatori e referenti della squadra."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pronto || sessione.isLoading || statoSalvato.isLoading) {
|
if (!pronto || sessione.isLoading || statoSalvato.isLoading) {
|
||||||
return (
|
return (
|
||||||
<Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />
|
<Blocco icona={<Radio className="h-6 w-6" />} titolo="Scout live" testo="Caricamento…" />
|
||||||
|
|||||||
Reference in New Issue
Block a user