diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 18e089a..639fed3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,11 @@ versioni `0.y.z` erano pre-release. - **Keepalive giornaliero** — un Vercel Cron Job chiama `/api/public/keepalive` una volta al giorno con una lettura minima, per evitare che il progetto Supabase free tier vada in pausa dopo 7 giorni senza richieste API. +- **Scheda evento** — gli eventi extra-campo (es. cena di squadra) hanno ora una pagina di + dettaglio propria, come allenamenti e partite, con l'elenco nominativo di chi ha risposto + presente/assente/forse invece del solo conteggio in card. "Infortunato" non compare più + tra le opzioni né nei badge/riepiloghi per questi eventi, dato che non è una risposta + pertinente. ### Modificato diff --git a/docs/modules/badge.md b/docs/modules/badge.md index 9e5bb7c..9354fc9 100644 --- a/docs/modules/badge.md +++ b/docs/modules/badge.md @@ -408,11 +408,12 @@ meccanismo: `scritture.test.ts`). Non sfruttabile da un utente normale (l'app manda solo le 5 categorie valide), stesso tipo di gap "solo applicativo, non a DB" del punto sotto sul votato/convocato. - **`conteggioTurni()` non filtra per tipo evento** (`palloni-core.ts:70-82`), a differenza di - `eventiPalloni()` che scarta i compleanni. Un turno registrato per errore su un evento fuori - dal dominio "richiede i palloni" conterebbe comunque per il badge Sherpa dei palloni. Rischio - teorico basso (l'UI non offre questa combinazione), comportamento pinnato da un test dedicato - in `palloni-core.test.ts` così che un domani, se serve stringere, non lo si scopra rompendo un - test esistente ma leggendo perché quel test lo dimostrava apposta. + `eventiPalloni()` che include solo partite e allenamenti. Un turno registrato per errore su + un evento fuori dal dominio "richiede i palloni" conterebbe comunque per il badge Sherpa dei + palloni. Rischio teorico basso — l'UI non offre questa combinazione, dato che + `TurnoPalloni`/`completaTurni()`/i promemoria ora si fermano a `eventiPalloni()` — comportamento + pinnato da un test dedicato in `palloni-core.test.ts` così che un domani, se serve stringere, + non lo si scopra rompendo un test esistente ma leggendo perché quel test lo dimostrava apposta. --- diff --git a/docs/modules/calendario.md b/docs/modules/calendario.md index ef94a6e..db72e97 100644 --- a/docs/modules/calendario.md +++ b/docs/modules/calendario.md @@ -4,7 +4,8 @@ **File principali:** `src/lib/eventi.ts`, `src/lib/eventi.server.ts`, `src/lib/calendario.ts` (griglia mensile condivisa), `src/routes/calendario.tsx` (vista mensile, tutti), `src/routes/eventi.tsx` (creazione/modifica, admin e allenatori), `src/components/crapp/EventoCard.tsx` -(card condivisa) +(card condivisa), `src/routes/allenamento.$id.tsx`, `src/routes/partita.$id.tsx`, +`src/routes/evento.$id.tsx` (schede di dettaglio, una per tipo) **Test:** `test/unit/eventi.test.ts`, `test/unit/calendario.test.ts` --- @@ -44,8 +45,15 @@ Entrambe leggono la stessa cache (`useEventi()`, `EVENTI_KEY`, `staleTime` 10 mi calendario cambia raramente). `EventoCard.tsx` è la card riusata da entrambe le schermate; `linkPerEvento()` decide dove porta il click — `/partita/$id` per una partita (con `/partita-csi/$id` come alternativa "solo CSI" quando non c'è un evento collegato, vedi -`collegamento-csi.md`), `/allenamento/$id` per un allenamento, nessun link per eventi ed -eventi virtuali (compleanni). +`collegamento-csi.md`), `/allenamento/$id` per un allenamento, `/evento/$id` per un evento +extra-campo, nessun link per gli eventi virtuali (compleanni), che non hanno una riga in +`eventi_app`. + +Le tre schede di dettaglio (`allenamento.$id.tsx`, `partita.$id.tsx`, `evento.$id.tsx`) +condividono la stessa struttura — intestazione, note, conferme, `RosaPresenze` per l'elenco +nominativo di chi ha risposto — con `TurnoPalloni` in più per allenamenti e partite (vedi +`palloni.md`, non pertinente per gli eventi extra-campo) e lo stato `infortunato` escluso +dalle risposte solo sulla scheda evento (vedi `presenze.md`). ## Lettura lato server diff --git a/docs/modules/palloni.md b/docs/modules/palloni.md index 70ae62b..2441908 100644 --- a/docs/modules/palloni.md +++ b/docs/modules/palloni.md @@ -11,7 +11,8 @@ Gestire un turno a rotazione condiviso per chi porta e riporta i palloni ad allenamenti e partite, con proposta automatica, possibilità di modifica manuale e promemoria push il -giorno stesso. +giorno stesso. Gli eventi extra-campo (`tipo === "evento"`, es. cena di squadra) e i +compleanni non c'entrano: non richiedono palloni. --- @@ -25,11 +26,14 @@ compaiono. ## Implementazione -- `completaTurni()` (`palloni-core.ts`) propone, per ogni **partita** o evento extra senza - turno già salvato, il candidato con meno turni fatti, poi quello che non lo fa da più - tempo, poi per ordine alfabetico — un algoritmo greedy, non un ordine fisso né solo per - data. Gli **allenamenti** non ricevono proposta automatica: restano «da assegnare» finché - qualcuno non sceglie un incaricato in `TurnoPalloni` (scelta della squadra). +- `eventiPalloni()` (`palloni-core.ts`) è il filtro comune a tutta la pipeline (proposte, + conteggi, serie, promemoria): include solo `tipo === "partita"` e `tipo === "allenamento"`, + scarta eventi extra-campo e compleanni. +- `completaTurni()` (`palloni-core.ts`) propone, per ogni **partita** senza turno già salvato, + il candidato con meno turni fatti, poi quello che non lo fa da più tempo, poi per ordine + alfabetico — un algoritmo greedy, non un ordine fisso né solo per data. Gli **allenamenti** + non ricevono proposta automatica: restano «da assegnare» finché qualcuno non sceglie un + incaricato in `TurnoPalloni` (scelta della squadra). - `useAssegnaTurno()` (`palloni.ts`) conferma una proposta o riassegna manualmente, con upsert su `evento_id`. - Il conteggio "quante volte hai portato i palloni" mostrato nel profilo e nei badge è @@ -71,10 +75,12 @@ nessuna chiamata di rete. Stesso meccanismo di `apri-sondaggio` (vedi - La rotazione non considera le assenze dichiarate: può proporre il turno a chi ha risposto "assente" o "infortunato" per quell'evento. - **`conteggioTurni()` non filtra per tipo evento** (a differenza di `eventiPalloni()`, che - scarta i compleanni): guarda solo `e.data < oggi`. Un turno registrato per errore su un - evento fuori dal dominio "richiede i palloni" conterebbe comunque per il badge Sherpa dei - palloni (`badge.md` § Problemi noti). Rischio basso — l'UI non offre questa combinazione — ma - il comportamento attuale è pinnato da un test dedicato in `palloni-core.test.ts`. + include solo partite e allenamenti): guarda solo `e.data < oggi`. Un turno registrato per + errore su un evento fuori dal dominio "richiede i palloni" conterebbe comunque per il badge + Sherpa dei palloni (`badge.md` § Problemi noti). Rischio basso — l'UI non offre questa + combinazione, dato che né `TurnoPalloni` né i promemoria automatici propongono più un turno + per eventi extra-campo o compleanni — ma il comportamento attuale è pinnato da un test + dedicato in `palloni-core.test.ts`. --- diff --git a/docs/modules/presenze.md b/docs/modules/presenze.md index faf0c29..d6c282b 100644 --- a/docs/modules/presenze.md +++ b/docs/modules/presenze.md @@ -24,8 +24,11 @@ attuale. Stati possibili (`Stato` in `src/lib/crapp-data.ts`): `presente`, `assente`, `forse`, `ritardo`, `infortunato`. Solo `presente` e `ritardo` contano come presenza effettiva nelle statistiche. L'assenza di una riga per `(evento, giocatore)` equivale a "non ha ancora -risposto". Sulla card in home/calendario (`EventoCard`) lo stato `infortunato` è offerto solo -per partite e allenamenti; sugli eventi extra-campo non è selezionabile (non pertinente). +risposto". Lo stato `infortunato` è offerto solo per partite e allenamenti: sugli eventi +extra-campo (`tipo === "evento"`, es. cena di squadra) non è pertinente e resta escluso +ovunque — badge di riepilogo, gruppi elenco e bottoni di risposta — sia nella card +(`EventoCard`, tramite `includeInfortunato()` in `presenze.ts`) sia nella scheda di dettaglio +(`RosaPresenze`, prop `tipo`). --- @@ -41,8 +44,9 @@ risposte_presenze (Supabase) useRispostePresenze() → src/lib/presenze.ts (1 query per sessione, staleTime 5 min, ↓ legge tutta la tabella) RosaPresenze → src/components/crapp/RosaPresenze.tsx - ↑ montato da (riepilogo, bottoni di risposta, gruppi per stato) -allenamento.$id.tsx / partita.$id.tsx + ↑ montato da (riepilogo, bottoni di risposta, gruppi per stato; +allenamento.$id.tsx / prop `tipo` esclude "infortunato" per gli eventi extra-campo) +partita.$id.tsx / evento.$id.tsx EventoCard → src/components/crapp/EventoCard.tsx ↑ home / calendario (riga compatta di stati; senza `infortunato` se tipo `evento`) diff --git a/src/components/crapp/EventoCard.tsx b/src/components/crapp/EventoCard.tsx index 9d082d0..7b6cb51 100644 --- a/src/components/crapp/EventoCard.tsx +++ b/src/components/crapp/EventoCard.tsx @@ -15,7 +15,7 @@ import { Card } from "@/components/crapp/ui-bits"; import { formatData, statoMeta, type Stato } from "@/lib/crapp-data"; import type { Evento } from "@/lib/eventi"; import { inRosa, useGiocatoriSquadra } from "@/lib/giocatori-squadra"; -import { usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze"; +import { includeInfortunato, usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze"; import { useGiocatoreInCampo } from "@/lib/user-store"; import { dataOggi } from "@/lib/scout-live"; @@ -26,9 +26,7 @@ const tipoMeta = { compleanno: { label: "Compleanno", className: "bg-success text-success-foreground" }, } as const; -const statiSportivi: Stato[] = ["presente", "forse", "ritardo", "assente", "infortunato"]; -/** Eventi extra-campo (pizze, uscite…): l'infortunio non è una risposta pertinente. */ -const statiEventoExtra: Stato[] = ["presente", "forse", "ritardo", "assente"]; +const statiBase: Stato[] = ["presente", "forse", "ritardo", "assente"]; /** Icone compatte per la riga unica dei controlli presenza (mockup). */ const iconeStato: Record = { @@ -51,6 +49,9 @@ export function linkPerEvento(e: Evento) { label: "Dettaglio allenamento", }; } + if (e.tipo === "evento") { + return { to: "/evento/$id" as const, params: { id: e.id }, label: "Dettaglio evento" }; + } return undefined; } @@ -87,15 +88,11 @@ export function EventoCard({ const isCompleanno = evento.tipo === "compleanno"; const passato = evento.data < dataOggi(); const cliccabile = Boolean(linkTo); - /** Solo gli eventi extra-campo non hanno scheda dedicata: le note restano sulla card. */ + /** Anteprima delle note anche in card per gli eventi extra-campo (oltre al dettaglio). */ const noteCard = evento.tipo === "evento" ? evento.note.trim() : ""; - const stati = - evento.tipo === "evento" - ? // Se resta un vecchio "infortunato", mostra il bottone solo per poterlo togliere. - stato === "infortunato" - ? [...statiEventoExtra, "infortunato" as const] - : statiEventoExtra - : statiSportivi; + const stati = includeInfortunato(evento.tipo, stato) + ? [...statiBase, "infortunato" as const] + : statiBase; if (isCompleanno) { return ( diff --git a/src/components/crapp/RosaPresenze.tsx b/src/components/crapp/RosaPresenze.tsx index bbbc15b..f02d065 100644 --- a/src/components/crapp/RosaPresenze.tsx +++ b/src/components/crapp/RosaPresenze.tsx @@ -6,6 +6,7 @@ import { Card } from "@/components/crapp/ui-bits"; import { Avatar } from "@/components/crapp/Avatar"; import { Barra } from "@/components/motion/Barra"; import { statoMeta, type Giocatore, type Stato } from "@/lib/crapp-data"; +import type { EventoTipo } from "@/lib/eventi"; import { usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze"; import { useAnagraficaRosa } from "@/lib/rosa"; import { useGiocatoreBase, useGiocatoreInCampo } from "@/lib/user-store"; @@ -15,7 +16,16 @@ import { dataOggi } from "@/lib/scout-live"; const ordine: Stato[] = ["presente", "ritardo", "forse", "infortunato", "assente"]; -export function RosaPresenze({ eventoId, data }: { eventoId: string; data: string }) { +export function RosaPresenze({ + eventoId, + data, + tipo = "allenamento", +}: { + eventoId: string; + data: string; + /** Negli eventi extra-campo "infortunato" non è pertinente: niente badge né opzione. */ + tipo?: EventoTipo; +}) { const { risposte, isPending } = usePresenzeEvento(eventoId); const salva = useSalvaPresenza(); // Solo `.id`/`.nome` servono qui: `useGiocatoreBase`/`useAnagraficaRosa` bastano, @@ -32,6 +42,7 @@ export function RosaPresenze({ eventoId, data }: { eventoId: string; data: strin const risposteN = rosa.length - mancanti.length; const perc = rosa.length ? Math.round((risposteN / rosa.length) * 100) : 0; const daSollecitare = mancanti.length + rosa.filter((g) => risposte[g.id] === "forse").length; + const ordineVisibile = tipo === "evento" ? ordine.filter((s) => s !== "infortunato") : ordine; async function sollecita() { setSollecito(true); @@ -67,7 +78,7 @@ export function RosaPresenze({ eventoId, data }: { eventoId: string; data: strin
- {ordine.map((s) => { + {ordineVisibile.map((s) => { const n = rosa.filter((g) => risposte[g.id] === s).length; return (
- {ordine.map((s) => { + {ordineVisibile.map((s) => { const attivo = risposte[io.id] === s; return (