diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6a50f69..077c9a8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -17,6 +17,8 @@ all'indietro. - **Notifiche in-app** — tolta la campanella in alto a destra: il numero delle notifiche è ora un pallino rosso sull'avatar del profilo. Toccando il pallino si apre l'elenco (leggi ed 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. ## [0.9.2] - 2026-09-10 diff --git a/docs/modules/collegamento-csi.md b/docs/modules/collegamento-csi.md index ec894b5..97e5de7 100644 --- a/docs/modules/collegamento-csi.md +++ b/docs/modules/collegamento-csi.md @@ -43,6 +43,15 @@ un `project_id`), nessun codice le chiama direttamente. `project_id` (767), `CSI_COPPA_PROJECT_ID` (848) e `team_id` (3359) sono costanti in `src/lib/csi-core.ts`. +**Stagione 2026/27 non ancora sul portale (verificato il 24/09/2026).** Il CSI non ha ancora +creato le pagine di campionato e Coppa 2026/27: le uniche competizioni della squadra sono +ancora `767` e `848`, con gare da novembre 2025 a maggio 2026. Per questo `/classifica` +mostra «Stagione 2025/26» mentre Squadra e Calendario, che hanno la stagione scritta nel +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. + 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 `league_details.php?project_id=…` per ogni competizione a cui la squadra è iscritta — @@ -155,7 +164,8 @@ useCsi() → src/lib/csi.ts (React Query, staleTime 6h) ↓ /classifica → src/routes/classifica.tsx (tab "Classifica": Coppa sopra, Girone sotto; tab "Storico partite": ogni squadra col proprio logo, - chevron di dettaglio sulle gare cliccabili) + chevron di dettaglio sulle gare cliccabili, gare divise + per stagione; stagione anche nel sottotitolo dell'header) CSI (portale, 3 endpoint) ↓ fetch server-side on-demand, cache per-partita 6 ore @@ -239,6 +249,9 @@ DettaglioCsiEsteso → src/components/crapp/DettaglioCsi.tsx (formazio Il check con `CSI_LIVE=1` serve a scoprire il problema di parsing. 2. **`project_id` è legato alla stagione.** Per il 2026/27 servirà un nuovo id (vedi "Sorgente dati" sopra per come ritrovarlo). Oggi va aggiornato a mano in `csi-core.ts`. + L'etichetta «Stagione 2025/26» di `/classifica` invece non va toccata: il portale non + espone il nome della stagione, quindi `stagioneDi()`/`stagioneDaPartite()` la ricavano + dalla data delle gare (l'anno cambia ad agosto) e segue da sola il nuovo `project_id`. 3. **La cache vive nel processo del server.** Si perde a ogni cold start e non è condivisa tra istanze — vale sia per `/api/public/csi` sia per la `Map` per-partita di `/api/public/csi-partita/$id`. Sufficiente per una squadra; se serve di più, spostare i diff --git a/src/lib/csi-core.ts b/src/lib/csi-core.ts index 54e9dbc..3120c33 100644 --- a/src/lib/csi-core.ts +++ b/src/lib/csi-core.ts @@ -21,6 +21,43 @@ export const CSI_TEAM_ID = 3359; export const CSI_GIRONE = "Girone B"; export const CSI_NOME_SQUADRA = "C.R.A.P. Volley"; +/** + * Stagione sportiva di una data `YYYY-MM-DD`, nel formato "2025/26". Il portale CSI non + * espone il nome della stagione (né nella classifica né nel calendario della squadra): + * si ricava dalla data della gara, con l'anno che cambia ad agosto — le gare di + * settembre-dicembre aprono la stagione, quelle di gennaio-luglio la chiudono. + */ +export function stagioneDi(dataIso: string): string { + const anno = Number(dataIso.slice(0, 4)); + const mese = Number(dataIso.slice(5, 7)); + const inizio = mese >= 8 ? anno : anno - 1; + return `${inizio}/${String((inizio + 1) % 100).padStart(2, "0")}`; +} + +/** Stagione del campionato letto dal CSI: quella della prima gara, null se non ce ne sono. */ +export function stagioneDaPartite(partite: ReadonlyArray<{ data: string }>): string | null { + const date = partite.map((p) => p.data).filter((d) => /^\d{4}-\d{2}/.test(d)); + if (date.length === 0) return null; + return stagioneDi(date.reduce((a, b) => (a < b ? a : b))); +} + +/** + * Storico partite diviso per stagione, nell'ordine in cui arrivano: le gare scoutate in + * locale possono essere di stagioni diverse, quelle CSI sono tutte dello stesso campionato. + */ +export function raggruppaPerStagione( + match: readonly T[], +): Array<{ stagione: string; match: T[] }> { + const gruppi: Array<{ stagione: string; match: T[] }> = []; + for (const m of match) { + const stagione = stagioneDi(m.data); + const gruppo = gruppi.find((g) => g.stagione === stagione); + if (gruppo) gruppo.match.push(m); + else gruppi.push({ stagione, match: [m] }); + } + return gruppi; +} + export const urlClassifica = (projectId = CSI_PROJECT_ID) => `${CSI_BASE}/components/project-sheets.php?project_id=${projectId}`; export const urlPartite = (teamId = CSI_TEAM_ID) => diff --git a/src/routes/classifica.tsx b/src/routes/classifica.tsx index 200ff41..c0208ba 100644 --- a/src/routes/classifica.tsx +++ b/src/routes/classifica.tsx @@ -7,7 +7,13 @@ import { PageHeader, TeamLogo } from "@/components/crapp/ui-bits"; import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni"; import { useScoutMatches } from "@/lib/scout-store"; import { useCsi } from "@/lib/csi"; -import { isNostraSquadra, matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core"; +import { + isNostraSquadra, + matchDaPartitaCsi, + partiteGiocate, + raggruppaPerStagione, + stagioneDaPartite, +} from "@/lib/csi-core"; import { useVotiMvp, vincitoriMvp } from "@/lib/mvp-voti"; import { useEventi } from "@/lib/eventi"; import { LogoSquadra } from "@/components/crapp/DettaglioCsi"; @@ -127,6 +133,8 @@ function Classifica() { const classifica = useMemo(() => csi?.classifica ?? [], [csi]); const classificaCoppa = useMemo(() => csi?.classificaCoppa ?? [], [csi]); + // Tutte le gare, non solo quelle giocate: a inizio stagione il calendario c'è già. + const stagioneCsi = useMemo(() => stagioneDaPartite(csi?.partite ?? []), [csi]); const mvpPerMatch = useMemo(() => vincitoriMvp(votiMvp.data ?? []), [votiMvp.data]); const eventoIdPerData = useMemo( @@ -200,119 +208,127 @@ function Classifica() { Nessuna partita disponibile.

) : ( -
- {tuttiMatch.map((m) => { - const vinta = m.setNostri > m.setLoro; - const eventoId = eventoIdPerData.get(m.data); - const cliccabile = Boolean(eventoId) || !m.scout; - const casa = { - nome: m.casa ? NOME_NOI : m.avversario, - nostro: m.casa, - }; - const trasferta = { - nome: m.casa ? m.avversario : NOME_NOI, - nostro: !m.casa, - }; - // Badge e parziali in ordine casa–ospite; il colore resta sulla vittoria CRAP. - const setCasa = m.casa ? m.setNostri : m.setLoro; - const setOspite = m.casa ? m.setLoro : m.setNostri; - const contenuto = ( - <> -
-
-
- -

{casa.nome}

-
-
- -

{trasferta.nome}

-
-

- {formatData(m.data)} · MVP {m.mvp || "da votare"} - {m.scout ? " · scoutata" : ""} -

-
- - {setCasa}-{setOspite} - - {cliccabile ? ( - - ) : null} -
-
- {m.parziali.map((p, i) => { - const puntiCasa = m.casa ? p[0] : p[1]; - const puntiOspite = m.casa ? p[1] : p[0]; - const setVintoDaNoi = p[0] > p[1]; - return ( +
+ {raggruppaPerStagione(tuttiMatch).map(({ stagione, match }) => ( +
+

Stagione {stagione}

+ {match.map((m) => { + const vinta = m.setNostri > m.setLoro; + const eventoId = eventoIdPerData.get(m.data); + const cliccabile = Boolean(eventoId) || !m.scout; + const casa = { + nome: m.casa ? NOME_NOI : m.avversario, + nostro: m.casa, + }; + const trasferta = { + nome: m.casa ? m.avversario : NOME_NOI, + nostro: !m.casa, + }; + // Badge e parziali in ordine casa–ospite; il colore resta sulla vittoria CRAP. + const setCasa = m.casa ? m.setNostri : m.setLoro; + const setOspite = m.casa ? m.setLoro : m.setNostri; + const contenuto = ( + <> +
+
+
+ +

{casa.nome}

+
+
+ +

{trasferta.nome}

+
+

+ {formatData(m.data)} · MVP {m.mvp || "da votare"} + {m.scout ? " · scoutata" : ""} +

+
- {puntiCasa}-{puntiOspite} + {setCasa}-{setOspite} - ); - })} - {eventoId && !m.mvp ? ( - - Vota MVP - - ) : null} -
- - ); - const aria = `${casa.nome} vs ${trasferta.nome}: dettaglio partita`; - if (eventoId) { - return ( - - {contenuto} - - ); - } - // Senza evento CrAPP collegato (nessuna creazione automatica dal calendario - // CSI, vedi "Evoluzioni possibili"): le gare scoutate localmente non hanno un - // corrispettivo sul portale da mostrare, quelle CSI sì. - return m.scout ? ( -
- {contenuto} -
- ) : ( - - {contenuto} - - ); - })} + {cliccabile ? ( + + ) : null} +
+
+ {m.parziali.map((p, i) => { + const puntiCasa = m.casa ? p[0] : p[1]; + const puntiOspite = m.casa ? p[1] : p[0]; + const setVintoDaNoi = p[0] > p[1]; + return ( + + {puntiCasa}-{puntiOspite} + + ); + })} + {eventoId && !m.mvp ? ( + + Vota MVP + + ) : null} +
+ + ); + const aria = `${casa.nome} vs ${trasferta.nome}: dettaglio partita`; + if (eventoId) { + return ( + + {contenuto} + + ); + } + // Senza evento CrAPP collegato (nessuna creazione automatica dal calendario + // CSI, vedi "Evoluzioni possibili"): le gare scoutate localmente non hanno un + // corrispettivo sul portale da mostrare, quelle CSI sì. + return m.scout ? ( +
+ {contenuto} +
+ ) : ( + + {contenuto} + + ); + })} + + ))}
), [tuttiMatch, eventoIdPerData], @@ -322,7 +338,13 @@ function Classifica() { <>