From ae5d7900117b9a8eba19a1f4c75f9372f79c4fac Mon Sep 17 00:00:00 2001
From: Davide Grilli
Date: Thu, 24 Sep 2026 14:01:55 +0200
Subject: [PATCH] Mostra la stagione CSI nell'header di Campionato e divide lo
storico per stagione
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Il portale CSI non espone il nome della stagione: stagioneDi() la ricava dalla data
della gara (l'anno cambia ad agosto), stagioneDaPartite() dà quella del campionato e
raggruppaPerStagione() divide lo storico partite. Coperte da test in csi-core.test.ts.
Documentato che oggi Campionato mostra 2025/26 mentre Squadra e Calendario mostrano
2026/27: il CSI non ha ancora creato le pagine della nuova stagione.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
docs/CHANGELOG.md | 2 +
docs/modules/collegamento-csi.md | 15 +-
src/lib/csi-core.ts | 37 +++++
src/routes/classifica.tsx | 242 +++++++++++++++++--------------
test/unit/csi-core.test.ts | 33 +++++
5 files changed, 218 insertions(+), 111 deletions(-)
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.