3 Commits
Author SHA1 Message Date
davide 2f583ca97f Merge develop: versione 1.4.1 2026-10-19 08:31:53 +02:00
davideandClaude Sonnet 5.5 7996dd050e CHANGELOG: la correzione della card evento è parte della 1.4.1
Sposta la voce da «Non rilasciato» alla sezione 1.4.1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 08:31:10 +02:00
davideandClaude Sonnet 5.5 5ee148b88b Card evento filtrata sui convocati + copertura test per numero di convocati
- La card (Calendario e Home) conta «presenti/convocati» e mostra i pulsanti di presenza solo
  ai convocati; «Da confermare» in Home esclude gli eventi a cui non si è convocati.
- Nuove eConvocato() e rosaConvocata(): il filtro, prima ripetuto in tre punti, è una funzione
  pura usata da card, «Rosa e presenze» e convocatiEvento.
- Test per 0, 1, 2, 3, tutti e id sconosciuti su visibilità, convocazione e solleciti.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 08:29:36 +02:00
7 changed files with 100 additions and 12 deletions

No files matched your search

+4
View File
@@ -30,6 +30,10 @@ Nessuna migration.
sempre tutta la rosa, anche se ne era stato convocato uno solo). Il conteggio «Hanno risposto», 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` il pulsante «Sollecita» e il riquadro «La tua risposta» seguono la stessa regola; `convocati`
vuoto resta «tutta la rosa». vuoto resta «tutta la rosa».
- 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.0] - 2026-10-09 ## [1.4.0] - 2026-10-09
+4 -3
View File
@@ -13,7 +13,7 @@ import { Link } from "@tanstack/react-router";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Card } from "@/components/crapp/ui-bits"; import { Card } from "@/components/crapp/ui-bits";
import { formatData, statoMeta, type Stato } from "@/lib/crapp-data"; 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 { inRosa, useGiocatoriSquadra } from "@/lib/giocatori-squadra";
import { includeInfortunato, usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze"; import { includeInfortunato, usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze";
import { useGiocatoreInCampo } from "@/lib/user-store"; 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. // Rilevante perché ogni card monta questo hook: il Calendario ne rende diverse insieme.
const io = useGiocatoreInCampo(); const io = useGiocatoreInCampo();
const { righe: squadra } = useGiocatoriSquadra(); 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 stato = io ? risposte[io.id] : undefined;
const presentiVeri = rosa.filter( const presentiVeri = rosa.filter(
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo", (g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
@@ -187,7 +188,7 @@ export function EventoCard({
</p> </p>
) : null} ) : null}
{io ? ( {io && eConvocato(evento, io.id) ? (
<div <div
className={cn("pointer-events-auto flex w-full gap-1", metaStato ? "mt-2.5" : "mt-3")} className={cn("pointer-events-auto flex w-full gap-1", metaStato ? "mt-2.5" : "mt-3")}
role="group" role="group"
+2 -3
View File
@@ -6,7 +6,7 @@ import { Card } from "@/components/crapp/ui-bits";
import { Avatar } from "@/components/crapp/Avatar"; import { Avatar } from "@/components/crapp/Avatar";
import { Barra } from "@/components/motion/Barra"; import { Barra } from "@/components/motion/Barra";
import { statoMeta, type Giocatore, type Stato } from "@/lib/crapp-data"; import { statoMeta, type Giocatore, type Stato } from "@/lib/crapp-data";
import type { EventoTipo } from "@/lib/eventi"; import { rosaConvocata, type EventoTipo } from "@/lib/eventi";
import { usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze"; import { usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze";
import { useAnagraficaRosa } from "@/lib/rosa"; import { useAnagraficaRosa } from "@/lib/rosa";
import { useGiocatoreBase, useGiocatoreInCampo } from "@/lib/user-store"; import { useGiocatoreBase, useGiocatoreInCampo } from "@/lib/user-store";
@@ -38,8 +38,7 @@ export function RosaPresenze({
const mittente = useGiocatoreBase(); const mittente = useGiocatoreBase();
const puoSollecitare = usePuoGestireEventi(); const puoSollecitare = usePuoGestireEventi();
const anagrafica = useAnagraficaRosa(); const anagrafica = useAnagraficaRosa();
const rosa = const rosa = rosaConvocata(anagrafica, convocati);
convocati.length === 0 ? anagrafica : anagrafica.filter((g) => convocati.includes(g.id));
const [sollecito, setSollecito] = useState(false); const [sollecito, setSollecito] = useState(false);
const passato = data < dataOggi(); const passato = data < dataOggi();
+15 -4
View File
@@ -90,6 +90,11 @@ export function useEventi() {
return { ...query, eventi: query.data ?? [] }; return { ...query, eventi: query.data ?? [] };
} }
/** Il giocatore è tra i convocati (elenco vuoto = tutta la rosa). */
export function eConvocato(evento: Pick<Evento, "convocati">, 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 * 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. * eventi (admin e allenatori) li vede sempre, convocato o no.
@@ -99,8 +104,9 @@ export function eventoVisibile(
giocatoreId: string | null, giocatoreId: string | null,
vedeTutto: boolean, vedeTutto: boolean,
): boolean { ): boolean {
if (vedeTutto || evento.convocati.length === 0) return true; if (vedeTutto) return true;
return giocatoreId !== null && evento.convocati.includes(giocatoreId); if (evento.convocati.length === 0) return true;
return giocatoreId !== null && eConvocato(evento, giocatoreId);
} }
export function useEvento(id: string) { export function useEvento(id: string) {
@@ -211,6 +217,11 @@ export function compleanniEventi(
/** Rosa convocata per un evento: se non specificata vale tutta la rosa. */ /** Rosa convocata per un evento: se non specificata vale tutta la rosa. */
export function convocatiEvento(evento: Evento | null, rosa: Giocatore[]) { export function convocatiEvento(evento: Evento | null, rosa: Giocatore[]) {
if (!evento || evento.convocati.length === 0) return rosa; return rosaConvocata(rosa, evento?.convocati ?? []);
return rosa.filter((g) => evento.convocati.includes(g.id)); }
/** Restringe una rosa ai convocati indicati: elenco vuoto = tutta la rosa. */
export function rosaConvocata<T extends { id: string }>(rosa: T[], convocati: string[]): T[] {
if (convocati.length === 0) return rosa;
return rosa.filter((g) => convocati.includes(g.id));
} }
+2 -2
View File
@@ -11,7 +11,7 @@ import { Barra } from "@/components/motion/Barra";
import { Numero } from "@/components/motion/Numero"; import { Numero } from "@/components/motion/Numero";
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer"; import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi"; 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 { useEventiVisibili } from "@/lib/eventi-visibili";
import { useRispostePresenze } from "@/lib/presenze"; import { useRispostePresenze } from "@/lib/presenze";
import { useIo, useObiettivi } from "@/lib/rosa"; import { useIo, useObiettivi } from "@/lib/rosa";
@@ -55,7 +55,7 @@ function Index() {
const prossimo = prossimi[0] ?? null; const prossimo = prossimi[0] ?? null;
const daConfermare = prossimi const daConfermare = prossimi
.slice(1) .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 linkProssimo = prossimo ? linkPerEvento(prossimo) : null;
const { data: csi } = useCsi(); const { data: csi } = useCsi();
const noi = csi?.classifica.find((r) => isNostraSquadra(r.squadra)); const noi = csi?.classifica.find((r) => isNostraSquadra(r.squadra));
+41
View File
@@ -7,7 +7,9 @@ import {
convocatiEvento, convocatiEvento,
daCategoria, daCategoria,
daRiga, daRiga,
eConvocato,
eventoVisibile, eventoVisibile,
rosaConvocata,
eventoVuoto, eventoVuoto,
type RigaEvento, type RigaEvento,
} from "@/lib/eventi"; } 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"] }, null, false), false, "senza slot no");
assert.equal(eventoVisibile({ convocati: ["g1"] }, "g2", true), true, "admin/allenatore sempre"); assert.equal(eventoVisibile({ convocati: ["g1"] }, "g2", true), true, "admin/allenatore sempre");
assert.equal(eventoVisibile({ convocati: [] }, "g2", false), true, "vuoto = tutta la rosa"); 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");
+32
View File
@@ -294,3 +294,35 @@ assert.equal(
); );
console.log("presenze: ok"); 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",
);
}