From 5ca081a6af33e180bd6f31bf9ecc83a2104822cf Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 19 Oct 2026 08:12:02 +0200 Subject: [PATCH] =?UTF-8?q?Eventi=20visibili=20solo=20ai=20convocati;=20?= =?UTF-8?q?=C2=ABRosa=20e=20presenze=C2=BB=20filtrata=20sui=20convocati?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - «Rosa e presenze» (allenamento, partita, evento) elenca, conta e solleciti solo i convocati invece di tutta la rosa; il riquadro «La tua risposta» sparisce per i non convocati. - Calendario, Home e elenco eventi mostrano un evento con convocati solo a chi è convocato; admin e allenatori lo vedono sempre, `convocati` vuoto = tutti (eventoVisibile). - Filtro solo lato interfaccia: la RLS di eventi_app non cambia. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 13 +++++++++++++ src/components/crapp/RosaPresenze.tsx | 9 +++++++-- src/lib/eventi-visibili.ts | 14 ++++++++++++++ src/lib/eventi.ts | 13 +++++++++++++ src/routes/allenamento.$id.tsx | 7 ++++++- src/routes/calendario.tsx | 5 +++-- src/routes/eventi.tsx | 4 ++-- src/routes/evento.$id.tsx | 7 ++++++- src/routes/index.tsx | 5 +++-- src/routes/partita.$id.tsx | 7 ++++++- test/unit/eventi.test.ts | 8 ++++++++ 11 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/lib/eventi-visibili.ts 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");