diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 3641570..08d4d66 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -13,6 +13,19 @@ versioni `0.y.z` erano pre-release.
## [Non rilasciato]
+### Modificato
+
+- Un evento con dei convocati compare nel Calendario, in Home e nell'elenco eventi solo ai
+ convocati. Admin e allenatori lo vedono sempre; `convocati` vuoto = visibile a tutti. È un
+ filtro dell'interfaccia: i dati restano leggibili dal database (RLS di `eventi_app` invariata).
+
+### Corretto
+
+- «Rosa e presenze» di allenamenti, partite ed eventi mostra solo i convocati (prima elencava
+ sempre tutta la rosa, anche se ne era stato convocato uno solo). Il conteggio «Hanno risposto»,
+ il pulsante «Sollecita» e il riquadro «La tua risposta» seguono la stessa regola; `convocati`
+ vuoto resta «tutta la rosa».
+
## [1.4.0] - 2026-10-09
Gli admin e gli allenatori vengono avvisati quando una presenza cambia a ridosso di una partita o di un allenamento.
diff --git a/src/components/crapp/RosaPresenze.tsx b/src/components/crapp/RosaPresenze.tsx
index ad9c9cf..e4a5595 100644
--- a/src/components/crapp/RosaPresenze.tsx
+++ b/src/components/crapp/RosaPresenze.tsx
@@ -20,9 +20,12 @@ export function RosaPresenze({
eventoId,
data,
tipo = "allenamento",
+ convocati = [],
}: {
eventoId: string;
data: string;
+ /** Id dei convocati: vuoto = tutta la rosa (come `convocatiEvento()`). */
+ convocati?: string[];
/** Negli eventi extra-campo "infortunato" non è pertinente: niente badge né opzione. */
tipo?: EventoTipo;
}) {
@@ -34,7 +37,9 @@ export function RosaPresenze({
// Il nome di chi sollecita: admin o allenatore, che non ha una risposta propria (DD-034).
const mittente = useGiocatoreBase();
const puoSollecitare = usePuoGestireEventi();
- const rosa = useAnagraficaRosa();
+ const anagrafica = useAnagraficaRosa();
+ const rosa =
+ convocati.length === 0 ? anagrafica : anagrafica.filter((g) => convocati.includes(g.id));
const [sollecito, setSollecito] = useState(false);
const passato = data < dataOggi();
@@ -97,7 +102,7 @@ export function RosaPresenze({
- {io ? (
+ {io && rosa.some((g) => g.id === io.id) ? (
La tua risposta
diff --git a/src/lib/eventi-visibili.ts b/src/lib/eventi-visibili.ts
new file mode 100644
index 0000000..dccdaa5
--- /dev/null
+++ b/src/lib/eventi-visibili.ts
@@ -0,0 +1,14 @@
+import { eventoVisibile, useEventi } from "./eventi";
+import { usePuoGestireEventi } from "./ruoli";
+import { useGiocatoreId } from "./user-store";
+
+/** Gli eventi che l'utente corrente può vedere (calendario, home, elenco). */
+export function useEventiVisibili() {
+ const { eventi, ...resto } = useEventi();
+ const giocatoreId = useGiocatoreId();
+ const vedeTutto = usePuoGestireEventi();
+ return {
+ ...resto,
+ eventi: eventi.filter((e) => eventoVisibile(e, giocatoreId, vedeTutto)),
+ };
+}
diff --git a/src/lib/eventi.ts b/src/lib/eventi.ts
index a68d851..2cf690d 100644
--- a/src/lib/eventi.ts
+++ b/src/lib/eventi.ts
@@ -90,6 +90,19 @@ export function useEventi() {
return { ...query, eventi: query.data ?? [] };
}
+/**
+ * Chi vede un evento: i convocati; se `convocati` è vuoto, tutta la rosa. Chi gestisce gli
+ * eventi (admin e allenatori) li vede sempre, convocato o no.
+ */
+export function eventoVisibile(
+ evento: Pick,
+ giocatoreId: string | null,
+ vedeTutto: boolean,
+): boolean {
+ if (vedeTutto || evento.convocati.length === 0) return true;
+ return giocatoreId !== null && evento.convocati.includes(giocatoreId);
+}
+
export function useEvento(id: string) {
const { eventi, ...resto } = useEventi();
return { ...resto, evento: eventi.find((e) => e.id === id) ?? null };
diff --git a/src/routes/allenamento.$id.tsx b/src/routes/allenamento.$id.tsx
index 2a6e031..f58adc6 100644
--- a/src/routes/allenamento.$id.tsx
+++ b/src/routes/allenamento.$id.tsx
@@ -93,7 +93,12 @@ function AllenamentoDetail() {
>
);
diff --git a/src/routes/calendario.tsx b/src/routes/calendario.tsx
index 80b730b..31f046e 100644
--- a/src/routes/calendario.tsx
+++ b/src/routes/calendario.tsx
@@ -8,7 +8,8 @@ import { molla, proietta } from "@/lib/molla";
import { useMotoRidotto } from "@/lib/motion";
import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
import { Card, PageHeader, Section } from "@/components/crapp/ui-bits";
-import { compleanniEventi, useEventi, type Evento } from "@/lib/eventi";
+import { compleanniEventi, type Evento } from "@/lib/eventi";
+import { useEventiVisibili } from "@/lib/eventi-visibili";
import { useAnagraficaRosa } from "@/lib/rosa";
import { usePuoGestireEventi } from "@/lib/ruoli";
import { giorniDelMese, giorniIT, mesiIT, pad2, useMeseNav } from "@/lib/calendario";
@@ -56,7 +57,7 @@ function Calendario() {
const [giornoSelezionato, setGiornoSelezionato] = useState(null);
const [drawerAperto, setDrawerAperto] = useState(false);
const puoGestire = usePuoGestireEventi();
- const { eventi } = useEventi();
+ const { eventi } = useEventiVisibili();
const rosa = useAnagraficaRosa({ conAllenatori: true });
// `useMotoRidotto` copre anche i device deboli (RAM bassa), non solo
// `prefers-reduced-motion`: disattiva anche lo swipe orizzontale tra mesi.
diff --git a/src/routes/eventi.tsx b/src/routes/eventi.tsx
index 0bfd065..bc65a71 100644
--- a/src/routes/eventi.tsx
+++ b/src/routes/eventi.tsx
@@ -1,3 +1,4 @@
+import { useEventiVisibili } from "@/lib/eventi-visibili";
import { useEffect, useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { CalendarPlus, ChevronLeft, ChevronRight, Loader2, Pencil, Trash2, X } from "lucide-react";
@@ -22,7 +23,6 @@ import {
daCategoria,
eventoVuoto,
useEliminaEvento,
- useEventi,
useSalvaEvento,
type CategoriaEvento,
type Evento,
@@ -65,7 +65,7 @@ function GestioneEventi() {
const io = useGiocatoreBase();
// Admin o allenatore (DD-034): la RLS di `eventi_app` (M21) dice lo stesso.
const puoGestire = usePuoGestireEventi();
- const { eventi, isPending, isError, error, refetch } = useEventi();
+ const { eventi, isPending, isError, error, refetch } = useEventiVisibili();
const { righe: squadra } = useGiocatoriSquadra();
const salva = useSalvaEvento();
const elimina = useEliminaEvento();
diff --git a/src/routes/evento.$id.tsx b/src/routes/evento.$id.tsx
index a3bf16a..9a5b4d3 100644
--- a/src/routes/evento.$id.tsx
+++ b/src/routes/evento.$id.tsx
@@ -88,7 +88,12 @@ function EventoDetail() {
>
);
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 83cde7c..0413975 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -11,7 +11,8 @@ import { Barra } from "@/components/motion/Barra";
import { Numero } from "@/components/motion/Numero";
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
-import { useEventi, type Evento } from "@/lib/eventi";
+import { type Evento } from "@/lib/eventi";
+import { useEventiVisibili } from "@/lib/eventi-visibili";
import { useRispostePresenze } from "@/lib/presenze";
import { useIo, useObiettivi } from "@/lib/rosa";
import { isAllenatore, nomeCompleto } from "@/lib/giocatori-squadra";
@@ -47,7 +48,7 @@ function Index() {
const base = useGiocatoreBase();
const giocatore = useIo();
const allenatore = isAllenatore(base);
- const { eventi } = useEventi();
+ const { eventi } = useEventiVisibili();
const { presenze } = useRispostePresenze();
const oggi = new Date().toISOString().slice(0, 10);
const prossimi: Evento[] = eventi.filter((e) => e.data >= oggi).slice(0, 3);
diff --git a/src/routes/partita.$id.tsx b/src/routes/partita.$id.tsx
index b79972d..8f6c5e2 100644
--- a/src/routes/partita.$id.tsx
+++ b/src/routes/partita.$id.tsx
@@ -297,7 +297,12 @@ function PartitaDetail() {
) : null}
>
);
diff --git a/test/unit/eventi.test.ts b/test/unit/eventi.test.ts
index 46b12b2..162247e 100644
--- a/test/unit/eventi.test.ts
+++ b/test/unit/eventi.test.ts
@@ -7,6 +7,7 @@ import {
convocatiEvento,
daCategoria,
daRiga,
+ eventoVisibile,
eventoVuoto,
type RigaEvento,
} from "@/lib/eventi";
@@ -123,3 +124,10 @@ assert.match(bozza.id, /^e[a-z0-9]+$/, "id generato dal client");
assert.notEqual(bozza.id, "", "ogni bozza ha un id");
console.log("eventi: ok");
+
+// --- eventoVisibile: i non convocati non vedono l'evento, chi gestisce sì -----
+assert.equal(eventoVisibile({ convocati: ["g1"] }, "g1", false), true, "il convocato lo vede");
+assert.equal(eventoVisibile({ convocati: ["g1"] }, "g2", false), false, "il non convocato no");
+assert.equal(eventoVisibile({ convocati: ["g1"] }, null, false), false, "senza slot no");
+assert.equal(eventoVisibile({ convocati: ["g1"] }, "g2", true), true, "admin/allenatore sempre");
+assert.equal(eventoVisibile({ convocati: [] }, "g2", false), true, "vuoto = tutta la rosa");