diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6a05990..4b5e28f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,13 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +### Corretto + +- La card dell'evento (Calendario e Home) conta «presenti/convocati» invece di dividere per tutta la + rosa, e i pulsanti di presenza compaiono solo ai convocati: un admin che è anche giocatore ma non + è convocato vede l'evento senza poter rispondere. In «Da confermare» (Home) non finiscono più gli + eventi a cui non sei convocato. + ## [1.4.1] - 2026-10-19 Gli eventi con dei convocati si vedono solo a loro, e «Rosa e presenze» elenca solo i convocati. diff --git a/src/components/crapp/EventoCard.tsx b/src/components/crapp/EventoCard.tsx index 36be015..478e37b 100644 --- a/src/components/crapp/EventoCard.tsx +++ b/src/components/crapp/EventoCard.tsx @@ -13,7 +13,7 @@ import { Link } from "@tanstack/react-router"; import { cn } from "@/lib/utils"; import { Card } from "@/components/crapp/ui-bits"; import { formatData, statoMeta, type Stato } from "@/lib/crapp-data"; -import type { Evento } from "@/lib/eventi"; +import { eConvocato, rosaConvocata, type Evento } from "@/lib/eventi"; import { inRosa, useGiocatoriSquadra } from "@/lib/giocatori-squadra"; import { includeInfortunato, usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze"; import { useGiocatoreInCampo } from "@/lib/user-store"; @@ -75,7 +75,8 @@ export function EventoCard({ // Rilevante perché ogni card monta questo hook: il Calendario ne rende diverse insieme. const io = useGiocatoreInCampo(); const { righe: squadra } = useGiocatoriSquadra(); - const rosa = squadra.filter(inRosa); + // Solo i convocati contano e rispondono (vuoto = tutta la rosa). + const rosa = rosaConvocata(squadra.filter(inRosa), evento.convocati); const stato = io ? risposte[io.id] : undefined; const presentiVeri = rosa.filter( (g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo", @@ -187,7 +188,7 @@ export function EventoCard({

) : null} - {io ? ( + {io && eConvocato(evento, io.id) ? (
convocati.includes(g.id)); + const rosa = rosaConvocata(anagrafica, convocati); const [sollecito, setSollecito] = useState(false); const passato = data < dataOggi(); diff --git a/src/lib/eventi.ts b/src/lib/eventi.ts index 2cf690d..a0f1477 100644 --- a/src/lib/eventi.ts +++ b/src/lib/eventi.ts @@ -90,6 +90,11 @@ export function useEventi() { return { ...query, eventi: query.data ?? [] }; } +/** Il giocatore è tra i convocati (elenco vuoto = tutta la rosa). */ +export function eConvocato(evento: Pick, giocatoreId: string): boolean { + return evento.convocati.length === 0 || evento.convocati.includes(giocatoreId); +} + /** * 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. @@ -99,8 +104,9 @@ export function eventoVisibile( giocatoreId: string | null, vedeTutto: boolean, ): boolean { - if (vedeTutto || evento.convocati.length === 0) return true; - return giocatoreId !== null && evento.convocati.includes(giocatoreId); + if (vedeTutto) return true; + if (evento.convocati.length === 0) return true; + return giocatoreId !== null && eConvocato(evento, giocatoreId); } export function useEvento(id: string) { @@ -211,6 +217,11 @@ export function compleanniEventi( /** Rosa convocata per un evento: se non specificata vale tutta la rosa. */ export function convocatiEvento(evento: Evento | null, rosa: Giocatore[]) { - if (!evento || evento.convocati.length === 0) return rosa; - return rosa.filter((g) => evento.convocati.includes(g.id)); + return rosaConvocata(rosa, evento?.convocati ?? []); +} + +/** Restringe una rosa ai convocati indicati: elenco vuoto = tutta la rosa. */ +export function rosaConvocata(rosa: T[], convocati: string[]): T[] { + if (convocati.length === 0) return rosa; + return rosa.filter((g) => convocati.includes(g.id)); } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 0413975..1838ff0 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -11,7 +11,7 @@ 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 { type Evento } from "@/lib/eventi"; +import { eConvocato, type Evento } from "@/lib/eventi"; import { useEventiVisibili } from "@/lib/eventi-visibili"; import { useRispostePresenze } from "@/lib/presenze"; import { useIo, useObiettivi } from "@/lib/rosa"; @@ -55,7 +55,7 @@ function Index() { const prossimo = prossimi[0] ?? null; const daConfermare = prossimi .slice(1) - .filter((e) => !giocatore || !presenze[e.id]?.[giocatore.id]); + .filter((e) => !giocatore || (eConvocato(e, giocatore.id) && !presenze[e.id]?.[giocatore.id])); const linkProssimo = prossimo ? linkPerEvento(prossimo) : null; const { data: csi } = useCsi(); const noi = csi?.classifica.find((r) => isNostraSquadra(r.squadra)); diff --git a/test/unit/eventi.test.ts b/test/unit/eventi.test.ts index 162247e..20bad8d 100644 --- a/test/unit/eventi.test.ts +++ b/test/unit/eventi.test.ts @@ -7,7 +7,9 @@ import { convocatiEvento, daCategoria, daRiga, + eConvocato, eventoVisibile, + rosaConvocata, eventoVuoto, type RigaEvento, } from "@/lib/eventi"; @@ -131,3 +133,42 @@ assert.equal(eventoVisibile({ convocati: ["g1"] }, "g2", false), false, "il non 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"); + +// --- eConvocato: elenco vuoto = tutti ---------------------------------------- +assert.equal(eConvocato({ convocati: ["g1"] }, "g1"), true, "convocato"); +assert.equal(eConvocato({ convocati: ["g1"] }, "g2"), false, "non convocato"); +assert.equal(eConvocato({ convocati: [] }, "g2"), true, "vuoto = tutti"); + +// --- Copertura per numero di convocati: 1, 2, 3, tutti, nessuno, id sconosciuti - +const squadra = ["g1", "g2", "g3", "g4"].map((id) => ({ id })); +const ids = (l: Array<{ id: string }>) => l.map((g) => g.id); +const casi: Array<{ nome: string; convocati: string[]; attesi: string[] }> = [ + { nome: "nessuno indicato = tutta la rosa", convocati: [], attesi: ["g1", "g2", "g3", "g4"] }, + { nome: "solo 1", convocati: ["g2"], attesi: ["g2"] }, + { nome: "2", convocati: ["g1", "g4"], attesi: ["g1", "g4"] }, + { nome: "3", convocati: ["g3", "g1", "g2"], attesi: ["g1", "g2", "g3"] }, + { nome: "tutti indicati", convocati: ["g1", "g2", "g3", "g4"], attesi: ["g1", "g2", "g3", "g4"] }, + { nome: "id sconosciuto ignorato", convocati: ["g2", "zz"], attesi: ["g2"] }, + { nome: "solo ids sconosciuti: nessuno", convocati: ["zz"], attesi: [] }, +]; +for (const { nome, convocati, attesi } of casi) { + assert.deepEqual(ids(rosaConvocata(squadra, convocati)), attesi, `rosaConvocata: ${nome}`); + const conv = { convocati }; + for (const { id } of squadra) { + const dentro = attesi.includes(id) || convocati.length === 0; + assert.equal( + eConvocato(conv, id), + convocati.length === 0 || convocati.includes(id), + `eConvocato ${id}: ${nome}`, + ); + assert.equal(eventoVisibile(conv, id, false), dentro, `visibile a ${id}: ${nome}`); + assert.equal( + eventoVisibile(conv, id, true), + true, + `admin/allenatore vede sempre (${id}): ${nome}`, + ); + } + assert.equal(eventoVisibile(conv, null, false), convocati.length === 0, `senza slot: ${nome}`); + assert.equal(eventoVisibile(conv, null, true), true, `admin senza slot: ${nome}`); +} +assert.equal(rosaConvocata([], ["g1"]).length, 0, "rosa vuota resta vuota"); diff --git a/test/unit/presenze.test.ts b/test/unit/presenze.test.ts index 0ba147b..a6a1172 100644 --- a/test/unit/presenze.test.ts +++ b/test/unit/presenze.test.ts @@ -294,3 +294,35 @@ assert.equal( ); console.log("presenze: ok"); + +// --- Solleciti per numero di convocati: 1, 2, 3, tutti ------------------------ +{ + const sq = (["g1", "g2", "g3", "g4"] as const).map((id) => ({ + id, + attivo: true, + tipo: "giocatore" as const, + })); + const nessuna: Array<{ giocatore_id: string; stato: string }> = []; + assert.deepEqual(destinatariSollecito(sq, nessuna, ["g2"]), ["g2"], "solo 1 convocato"); + assert.deepEqual(destinatariSollecito(sq, nessuna, ["g1", "g3"]), ["g1", "g3"], "2 convocati"); + assert.deepEqual( + destinatariSollecito(sq, nessuna, ["g1", "g2", "g4"]), + ["g1", "g2", "g4"], + "3 convocati", + ); + assert.deepEqual( + destinatariSollecito(sq, [{ giocatore_id: "g1", stato: "presente" }], ["g1", "g3"]), + ["g3"], + "il convocato che ha già risposto non va sollecitato", + ); + assert.deepEqual( + destinatariSollecito(sq, [{ giocatore_id: "g4", stato: "presente" }], ["g1"]), + ["g1"], + "chi ha risposto ma non è convocato non cambia i destinatari", + ); + assert.deepEqual( + destinatariSollecito(sq, nessuna, []), + ["g1", "g2", "g3", "g4"], + "nessuno = tutti", + ); +}