diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 077c9a8..a3b4cb8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,10 @@ all'indietro. elimina), toccando l'avatar si va al profilo come prima. - **Campionato** — l'header riporta la stagione delle gare CSI (es. «Stagione 2025/26») e lo storico partite è diviso per stagione. +- **Gestione eventi** — tolta la lista "Eventi in calendario": resta solo il calendario + mensile, che ora si scorre anche con lo swipe come quello della squadra. +- **Gestione eventi** — nel form il giorno toccato sul calendario è già fissato: resta da + scegliere solo l'ora («Cambia» per spostare l'evento in un altro giorno). ## [0.9.2] - 2026-09-10 diff --git a/docs/DESIGN_DECISIONS.md b/docs/DESIGN_DECISIONS.md index 912cad8..a0579a1 100644 --- a/docs/DESIGN_DECISIONS.md +++ b/docs/DESIGN_DECISIONS.md @@ -45,6 +45,10 @@ Serve a rispondere a domande del tipo: | [DD-027](#dd-027--chi-vota-deve-essere-convocato-non-solo-autenticato-come-sé-stesso) | Voto limitato ai convocati | | [DD-028](#dd-028--soglia-minima-di-campione-per-media-voto-e-mvp-in-home) | Soglia minima Media voto e MVP | | [DD-029](#dd-029--cancellare-un-evento-pulisce-a-cascata-i-dati-collegati) | Pulizia a cascata evento cancellato | +| [DD-030](#dd-030--centro-notifiche-in-app-storico-separato-dalla-push-letto-solo-con-rls) | Centro notifiche in-app | +| [DD-031](#dd-031--data-di-nascita-pubblica-una-colonna-sincronizzata-non-una-seconda-rls-aperta) | Nascita pubblica sincronizzata | +| [DD-032](#dd-032--la-stagione-csi-si-ricava-dalle-date-delle-gare) | Stagione CSI dalle date delle gare | +| [DD-033](#dd-033--gestione-eventi-solo-calendario-giorno-fissato-dal-tocco) | Gestione eventi solo da calendario | **In valutazione** @@ -1318,3 +1322,80 @@ reso una funzione richiamabile: resta SQL diretto nella migration. La copertura comportamento eseguendo lo stesso pattern SQL via `psql` nel container Postgres locale — scelta necessaria perché senza una funzione RPC, PostgREST non espone un update multi-tabella come questo. + +### DD-032 — La stagione CSI si ricava dalle date delle gare + +**Data:** 24 settembre 2026 +**Stato:** Accettata + +**Contesto** +La pagina Campionato (`/classifica`) non diceva a quale stagione si riferissero classifica e +storico partite. Il portale CSI non espone il nome della stagione da nessuna parte: +`project-sheets.php`, `league_details.php`, `team-main.php` e il feed +`getEventsByTeamId.php` riportano solo il nome della competizione (verificato il +24/09/2026). Il `project_id` cambia a ogni stagione ed è aggiornato a mano in +`csi-core.ts`. + +**Decisione** +`stagioneDi()` in `src/lib/csi-core.ts` ricava la stagione ("2025/26") dalla data di una +gara, con l'anno che cambia ad agosto; `stagioneDaPartite()` usa la prima gara del +campionato letto (tutte le gare, non solo quelle giocate, così a inizio stagione basta il +calendario). L'header di `/classifica` mostra quella stagione, e lo storico partite è +diviso per stagione con `raggruppaPerStagione()`: le gare dello Scout Live locale, usate +quando il CSI non ne ha, possono essere di stagioni diverse. + +**Alternative scartate** + +- Una costante `CSI_STAGIONE` accanto a `CSI_PROJECT_ID` → un secondo valore da ricordarsi + di aggiornare a ogni stagione, che può divergere dal `project_id`; ricavata dai dati + segue da sola. +- Usare la stagione "in corso" per la squadra (la data di oggi) → sbaglierebbe proprio nel + periodo di passaggio: a settembre 2026 il CSI mostra ancora i dati 2025/26. + +**Conseguenze** + +- Oggi Campionato mostra «Stagione 2025/26» mentre Squadra e Calendario, che hanno la + stagione scritta nel codice, mostrano «Stagione 2026/27»: il CSI non ha ancora creato le + pagine della nuova stagione. È voluto — Campionato riporta la stagione dei dati che + mostra (dettagli in [collegamento-csi.md](modules/collegamento-csi.md)). +- Quando si aggiornano i `project_id` l'etichetta si aggiorna senza toccare altro codice. + +**Riesame** +Se il CSI iniziasse a esporre il nome della stagione, leggerlo da lì; se una stagione CSI +dovesse iniziare prima di agosto, spostare la soglia in `stagioneDi()`. + +### DD-033 — Gestione eventi: solo calendario, giorno fissato dal tocco + +**Data:** 24 settembre 2026 +**Stato:** Accettata + +**Contesto** +`/eventi` mostrava sotto la griglia mensile anche la lista cronologica di tutti gli eventi +("Eventi in calendario"). La lista non aveva azioni proprie (toccare una riga apriva lo +stesso drawer del giorno) e allungava la pagina con informazioni già nel calendario. Nel +form, poi, il campo «Data» era già precompilato con il giorno toccato ma sembrava da +compilare: l'amministratore doveva scegliere solo l'ora. + +**Decisione** +La pagina mostra solo il calendario, che si scorre di mese in mese con le frecce o con lo +swipe (stessa fisica a molla di `/calendario`, disattivata con `useMotoRidotto()`). Nel form +il giorno compare come testo fisso e resta da scegliere l'ora; il campo data compare solo +toccando «Cambia», per spostare un evento in un altro giorno. + +**Alternative scartate** + +- Togliere del tutto il campo data → non si potrebbe più spostare un evento esistente, una + funzione che oggi c'è (regola anti-regressione). +- Tenere la lista ma più corta (solo i prossimi eventi) → i prossimi eventi sono già in home + e in `/calendario`; qui sarebbe stata un doppione. + +**Conseguenze** + +- Per trovare un evento lontano bisogna scorrere fino al suo mese; il totale degli eventi + resta nel sottotitolo dell'header. +- Caricamento ed errore di `useEventi()` compaiono dentro la card del calendario, dove prima + stava la lista. + +**Riesame** +Se gli eventi crescono tanto da rendere scomodo lo scorrere dei mesi, valutare una ricerca +per titolo invece di reintrodurre la lista completa. diff --git a/docs/modules/calendario.md b/docs/modules/calendario.md index ccf242e..2c172ad 100644 --- a/docs/modules/calendario.md +++ b/docs/modules/calendario.md @@ -27,14 +27,16 @@ scout e turno palloni — la maggior parte degli altri moduli dipende da un `eve modifica ed elimina un evento, sceglie i convocati (`convocatiEvento()`, vuoto = tutta la rosa). Da qui si distingue "partita" da "amichevole" tramite il flag `campionato` (`categoriaEvento()`/`daCategoria()` in `eventi.ts` convertono tra la categoria mostrata - in interfaccia e la coppia `{ tipo, campionato }` salvata nel database). Sopra alla lista - cronologica c'è una griglia mensile (stessa logica di `/calendario`, tramite le funzioni - condivise di `src/lib/calendario.ts`): ogni giorno è cliccabile, anche senza eventi, e apre - un drawer con gli eventi di quel giorno (modifica/elimina) e un bottone "Nuovo evento in - questo giorno" che apre il form con la data già precompilata. Creare, modificare ed - eliminare passano solo da lì: la lista cronologica sotto il calendario è un elenco senza - azioni dirette, cliccare una riga apre lo stesso drawer del giorno corrispondente (anche se - è in un mese diverso da quello mostrato sulla griglia) invece di duplicare matita/cestino. + in interfaccia e la coppia `{ tipo, campionato }` salvata nel database). La pagina mostra + solo una griglia mensile (stessa logica di `/calendario`, tramite le funzioni condivise di + `src/lib/calendario.ts`), che si scorre di mese in mese con le frecce o con lo swipe, con + la stessa fisica a molla di `/calendario`. Ogni giorno è cliccabile, anche senza eventi, e + apre un drawer con gli eventi di quel giorno (modifica/elimina) e un bottone "Nuovo evento + in questo giorno" che apre il form con la data già precompilata: creare, modificare ed + eliminare passano solo da lì. Nel form il giorno si legge come testo fisso e resta da + scegliere solo l'ora; il campo data compare solo toccando «Cambia», per spostare l'evento. Fino a 0.9.2 sotto la griglia c'era anche la lista + cronologica di tutti gli eventi, tolta perché ripeteva il calendario + ([DD-033](../DESIGN_DECISIONS.md#dd-033--gestione-eventi-solo-calendario-giorno-fissato-dal-tocco)). Entrambe leggono la stessa cache (`useEventi()`, `EVENTI_KEY`, `staleTime` 10 minuti: il calendario cambia raramente). `EventoCard.tsx` è la card riusata da entrambe le schermate; diff --git a/docs/modules/collegamento-csi.md b/docs/modules/collegamento-csi.md index 97e5de7..11adfc9 100644 --- a/docs/modules/collegamento-csi.md +++ b/docs/modules/collegamento-csi.md @@ -50,7 +50,7 @@ mostra «Stagione 2025/26» mentre Squadra e Calendario, che hanno la stagione s codice, mostrano già «Stagione 2026/27». È voluto: la pagina Campionato riporta la stagione dei dati CSI che sta mostrando, non quella in corso per la squadra. Quando il CSI pubblica la nuova stagione si aggiornano i due `project_id` (vedi sotto) e l'etichetta passa da sola -a 2026/27. +a 2026/27 ([DD-032](../DESIGN_DECISIONS.md#dd-032--la-stagione-csi-si-ricava-dalle-date-delle-gare)). Per ritrovare questi id a ogni cambio stagione: `components/team-main.php?team_id=3359` (dietro `team_details.php`) contiene una sezione "Campionati" con un link diff --git a/src/routes/eventi.tsx b/src/routes/eventi.tsx index 66fbb32..b2c643c 100644 --- a/src/routes/eventi.tsx +++ b/src/routes/eventi.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { CalendarPlus, ChevronLeft, ChevronRight, Loader2, Pencil, Trash2, X } from "lucide-react"; import { toast } from "sonner"; +import { AnimatePresence, motion } from "motion/react"; import { cn } from "@/lib/utils"; import { Campo, Card, classiInput, PageHeader, Section } from "@/components/crapp/ui-bits"; import { @@ -28,6 +29,8 @@ import { } from "@/lib/eventi"; import { useGiocatoreBase } from "@/lib/user-store"; import { useIsAdmin } from "@/lib/ruoli"; +import { molla, proietta } from "@/lib/molla"; +import { useMotoRidotto } from "@/lib/motion"; export const Route = createFileRoute("/eventi")({ head: () => ({ @@ -68,6 +71,7 @@ function GestioneEventi() { const [bozza, setBozza] = useState(null); const [daEliminare, setDaEliminare] = useState(null); const [confermaModifica, setConfermaModifica] = useState(false); + const [cambiaGiorno, setCambiaGiorno] = useState(false); const rosa = squadra.filter((g) => g.attivo); // SSR-safe: la data di oggi arriva solo dopo il mount. @@ -77,7 +81,8 @@ function GestioneEventi() { setOggi({ anno: d.getFullYear(), mese: d.getMonth(), giorno: d.getDate() }); }, []); const [giornoSelezionato, setGiornoSelezionato] = useState(null); - const { anno, mese, precedente, successivo } = useMeseNav(); + const ridotto = useMotoRidotto(); + const { anno, mese, direzione, precedente, successivo } = useMeseNav(); const { giorni, offsetLunedi } = giorniDelMese(anno, mese); if (!io || !admin) { @@ -154,11 +159,13 @@ function GestioneEventi() { function nuovoNelGiorno() { if (!giornoSelezionato) return; setBozza({ ...eventoVuoto(), data: giornoSelezionato }); + setCambiaGiorno(false); setGiornoSelezionato(null); } function modificaDalGiorno(e: Evento) { setBozza(e); + setCambiaGiorno(false); setGiornoSelezionato(null); } @@ -167,8 +174,6 @@ function GestioneEventi() { setGiornoSelezionato(null); } - // Non usa `eventiPerGiorno` (limitato al mese in vista sul calendario): il giorno - // selezionato può arrivare anche dalla lista sotto, per un mese diverso da quello mostrato. const eventiGiornoSelezionato = giornoSelezionato ? eventi.filter((e) => e.data === giornoSelezionato) : []; @@ -206,34 +211,82 @@ function GestioneEventi() { {g} ))} -
- {Array.from({ length: offsetLunedi }).map((_, i) => ( - - ))} - {Array.from({ length: giorni }).map((_, i) => { - const giorno = i + 1; - const eventiGiorno = eventiPerGiorno.get(giorno) ?? []; - const haEventi = eventiGiorno.length > 0; - const isOggi = - !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno; - return ( - - ); - })} + {/* Swipe tra mesi con la stessa fisica di `/calendario` (vedi il commento lì). */} +
+ + { + const arrivo = info.offset.x + proietta(info.velocity.x); + if (arrivo < -60) successivo(); + else if (arrivo > 60) precedente(); + }} + initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }} + animate={{ opacity: 1, x: 0 }} + exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }} + transition={ridotto ? { duration: 0.2 } : molla.foglio} + className="grid touch-pan-y grid-cols-7 gap-1" + > + {Array.from({ length: offsetLunedi }).map((_, i) => ( + + ))} + {Array.from({ length: giorni }).map((_, i) => { + const giorno = i + 1; + const eventiGiorno = eventiPerGiorno.get(giorno) ?? []; + const haEventi = eventiGiorno.length > 0; + const isOggi = + !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno; + return ( + + ); + })} + +
+ {isPending ? ( +

+ Carico gli eventi… +

+ ) : isError ? ( +
+

+ Non sono riuscito a caricare gli eventi + {error instanceof Error ? `: ${error.message}` : ""}. +

+ +
+ ) : ( +

+ Scorri a destra o sinistra per cambiare mese. Tocca un giorno per aggiungere, + modificare o eliminare. +

+ )} @@ -270,14 +323,34 @@ function GestioneEventi() {
- - aggiorna({ data: e.target.value })} - className={classiInput} - /> - + {/* Il giorno arriva già dal calendario: si mostra e basta, il campo data + compare solo per spostare l'evento. */} + {cambiaGiorno ? ( + + aggiorna({ data: e.target.value })} + className={classiInput} + /> + + ) : ( +
+ + Giorno + +
+ {formatData(bozza.data)} + +
+
+ )} ) : null} -
- {isPending ? ( -

- Carico gli eventi… -

- ) : isError ? ( -
-

- Non sono riuscito a caricare gli eventi - {error instanceof Error ? `: ${error.message}` : ""}. -

- -
- ) : ( -
- {eventi.map((e) => ( - - ))} -
- )} -
- !aperto && setGiornoSelezionato(null)}