Eventi visibili solo ai convocati; «Rosa e presenze» filtrata sui convocati

- «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 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-19 08:12:02 +02:00
1 parent 3bf07049e9
commit 5ca081a6af
11 files changed
+81 -11

No files matched your search

+13
View File
@@ -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.
+7 -2
View File
@@ -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({
</span>
</div>
{io ? (
{io && rosa.some((g) => g.id === io.id) ? (
<div className="mt-4 border-t border-border pt-3">
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
La tua risposta
+14
View File
@@ -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)),
};
}
+13
View File
@@ -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<Evento, "convocati">,
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 };
+6 -1
View File
@@ -93,7 +93,12 @@ function AllenamentoDetail() {
</Section>
<Section titolo="Rosa e presenze">
<RosaPresenze eventoId={evento.id} data={evento.data} tipo={evento.tipo} />
<RosaPresenze
eventoId={evento.id}
data={evento.data}
tipo={evento.tipo}
convocati={evento.convocati}
/>
</Section>
</>
);
+3 -2
View File
@@ -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<number | null>(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.
+2 -2
View File
@@ -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();
+6 -1
View File
@@ -88,7 +88,12 @@ function EventoDetail() {
</Section>
<Section titolo="Rosa e presenze">
<RosaPresenze eventoId={evento.id} data={evento.data} tipo={evento.tipo} />
<RosaPresenze
eventoId={evento.id}
data={evento.data}
tipo={evento.tipo}
convocati={evento.convocati}
/>
</Section>
</>
);
+3 -2
View File
@@ -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);
+6 -1
View File
@@ -297,7 +297,12 @@ function PartitaDetail() {
) : null}
<Section titolo="Rosa e presenze">
<RosaPresenze eventoId={evento.id} data={evento.data} tipo={evento.tipo} />
<RosaPresenze
eventoId={evento.id}
data={evento.data}
tipo={evento.tipo}
convocati={evento.convocati}
/>
</Section>
</>
);
+8
View File
@@ -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");