Spariscono dalla scheda del giocatore in /admin il blocco «Tesseramento CSI» e il salvataggio, il badge «Tesserato/Da tesserare» e il riquadro «Tesserati» della tab Squadra. L'export CSV non cambia. Le colonne numero_tessera e data_tessera restano a database, non più usate; nessuna migration. Aggiornati PROJECT_STATE, CHANGELOG, ARCHITECTURE, DATABASE, ROADMAP, i moduli profilo-giocatore, squadra e obiettivi-squadra, test/README e l'indice delle decisioni. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
15 KiB
Modulo — Obiettivi di squadra
Stato: implementato — mesi/scadenze dinamici, target stagionali fissi da rivedere a mano,
copertura test completa (unit + integration) su tutti e 10 gli obiettivi.
File principali: src/lib/obiettivi.ts, src/lib/rosa.ts (useObiettivi()),
src/components/crapp/ObiettivoDrawer.tsx (card di dettaglio)
Obiettivo
Mostrare traguardi collettivi (non individuali) che avanzano con il contributo di tutta la rosa — presenze, risposte alle convocazioni, pagelle, risultati di campionato — per motivare comportamenti di squadra oltre alla singola prestazione.
Dati
Nessuna tabella dedicata: ogni obiettivo è una funzione pura in obiettivi.ts
(obiettiviSquadra()) che legge dati già aggregati altrove (risposte_presenze,
pagelle_voti, i risultati ufficiali CSI, le serie di presenza). obiettiviOrdinati() li
ordina mettendo i completati in coda e gli altri per progresso decrescente.
Non c'è nessuno stato da tenere sincronizzato quando un evento viene cancellato: gli obiettivi
sono ricalcolati da zero a ogni render partendo dall'elenco eventi corrente, quindi un evento
sparito da eventi_app smette semplicemente di contare, senza bisogno di nessuna pulizia
esplicita. Il problema che sembrava riguardare gli obiettivi era in realtà nelle tabelle
collegate a un evento (presenze, pagelle, MVP, ecc.), che restavano orfane a database dopo la
cancellazione: risolto a livello database con un trigger (migration
m14_pulizia_dati_evento_cancellato, DD-029), non nel modulo Obiettivi.
obiettiviSquadra(rosa, ctx, oggi) accetta un terzo parametro opzionale oggi: Date (default
new Date()) per iniettare una data deterministica nei test — usato dai due obiettivi con mese
corrente dinamico (vedi sotto).
Obiettivi definiti
| id | Obiettivo | Calcolo | Target | Fonte |
|---|---|---|---|---|
o1 |
90% presenze del mese | presente/ritardo su partite+allenamenti del mese corrente; posti = convocati dell'evento, tutta la rosa se vuoto (dinamico) | 90% | risposte_presenze |
o2 |
Tutti rispondono alle convocazioni | risposte totali / eventi possibili (esclusi i compleanni) | 90% | risposte_presenze |
o7 |
Presenze collettive | % di presenze (presente/ritardo) su tutti gli allenamenti e le partite definiti, passati e futuri; posti = convocati dell'evento (tutta la rosa se vuoto) | 90 | eventi_app + risposte_presenze |
o12 |
Media pagelle da 7.5 | media di tutti i voti, arrotondata a una cifra decimale | 7.5 | pagelle_voti |
o13 |
200 pagelle compilate | conteggio voti | 200 | pagelle_voti |
o11 |
Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | serieAllenamenti |
o3 |
Prima vittoria del campionato | min(vittorie, 1) |
1 | JSON partite CSI (vedi sotto) |
o4 |
5 vittorie in campionato | min(vittorie, 5) |
5 | JSON partite CSI |
o5 |
10 vittorie in campionato | min(vittorie, 10) |
10 | JSON partite CSI |
o6 |
1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente la cui data e ora sono già passate (dinamico) | 1 | eventi_app |
Mostrati in squadra.tsx (elenco completo con barra di progresso) e in index.tsx (home: il
primo obiettivo non completato).
Ogni obiettivo è cliccabile: apre la card di dettaglio (ObiettivoDrawer, stesso drawer dei
badge) con stato attuale, come si calcola, cosa conta e cosa no, quando vale, da dove arrivano i
dati e un esempio con numeri. Per le due percentuali di presenze (o1, o7) la card distingue «Presenza persa» (assente, forse, infortunato, nessuna risposta: il posto resta vuoto) da «Non entra nel calcolo» (cene, eventi di squadra, compleanni). I testi stanno nel campo dettaglio di ogni obiettivo in
obiettivi.ts: chi cambia il calcolo o il target di un obiettivo aggiorna anche il suo
dettaglio, altrimenti la card mente. Un obiettivo che supera il 90% genera anche una notifica
smart (notifiche-smart.ts). I target fissi (200 pagelle, 7.5 di media, 1/5/10
vittorie) sono scelte editoriali da rivedere a mano a ogni stagione — nessuna configurazione o
UI per farlo, si cambia il numero in obiettivi.ts. Fa eccezione "Continuità di squadra"
(vedi sotto): il suo target ha un significato specifico, non va scalato come gli altri.
Obiettivi mensili — mese dinamico
o1 ("90% presenze del mese") e o6 ("1 evento di squadra al mese") si azzerano
automaticamente a ogni cambio mese: il mese di riferimento è calcolato dalla data corrente
(fuso Europe/Rome, meseCorrente(oggi)), non più una costante fissa. Per o1, titolo
("90% di presenze ad agosto" / "a settembre" / ...) e scadenza (ultimo giorno del mese)
seguono di conseguenza. I posti di ogni partita e allenamento sono i convocati (tutta la rosa se non ce
ne sono): chi non è convocato non pesa, mentre assente, forse, infortunato e nessuna risposta sono
presenze perse.
o6 si completa quando arriva l'ora dell'evento, non quando lo si definisce: conta solo
un evento di tipo "evento" del mese corrente il cui data + ora (fuso Europe/Rome,
adessoRoma(oggi)) è già passato; senza ora vale 00:00. Un evento futuro lascia l'obiettivo a
0/1 finché non scatta l'ora. Il valore si ricalcola al render: con l'app già aperta il
passaggio a completato compare al refresh successivo.
o7 ("Presenze collettive") invece non si azzera: copre tutta la stagione, cioè tutti gli
allenamenti e le partite in calendario, passati e futuri (le risposte sugli eventi futuri sono
conferme). Differisce da o1 solo per il periodo (stagione, non mese): anche in o1 i posti sono i
convocati di ciascun evento, così chi non è convocato non pesa; con la stessa funzione
percentualePresenze(). Non c'è un confine di stagione esplicito: conta ciò
che c'è in eventi_app.
o2 ("Tutti rispondono alle convocazioni") non si azzera — aggrega su tutti gli eventi in
programma, non solo quelli del mese corrente — ma la sua scadenza mostrata in interfaccia è
anch'essa l'ultimo giorno del mese corrente (fineMese(oggi)), non più una data fissa.
Continuità di squadra — il target 12 è il minimo per un 6vs6
Il target di 12 giocatori con almeno 3 allenamenti consecutivi (o11) non è arbitrario: è
il numero minimo di giocatori per schierare due sestetti (6 contro 6) in allenamento. A
differenza degli altri target fissi, non va scalato in proporzione alla rosa se questa cambia
dimensione — resta 12 finché l'obiettivo è "riuscire ad allenarsi in modo completo".
Dipende da serieAllenamenti (vedi Serie di presenze), calcolato sui dati
reali: un evento passato senza risposta vale come assenza e azzera la serie, quindi l'obiettivo
misura anche quanto la squadra risponde alle convocazioni, non solo la presenza fisica.
Vittorie in campionato (o3/o4/o5) — dipendenza dal portale CSI
Le vittorie (ctx.vittorie) arrivano dal JSON delle partite del portale CSI Bologna
(getEventsByTeamId.php, non la pagina HTML della classifica), tramite
partiteGiocate(csi.partite).filter(p => p.setNostri > p.setLoro) calcolato in
src/lib/rosa.ts (useObiettivi()). o3/o4/o5 sono lo stesso numero di vittorie letto a
tre soglie diverse (1/5/10), ciascuna cappata con Math.min — nessuna delle tre supera mai il
proprio target, nemmeno con più vittorie di quante ne servano.
Il limite: il parsing del JSON può rompersi in silenzio
result e partials nella risposta di getEventsByTeamId.php sono stringhe libere tipo
"3-1", lette con un'espressione regolare (punteggio()/parziali() in csi-core.ts). Se il
portale CSI cambiasse formato (es. "3:1", o un punteggio come oggetto invece che stringa), la
regex non troverebbe corrispondenza e la partita risulterebbe "non ancora giocata" — senza
errori. Se la risposta cambiasse forma radicalmente (non più un array), partiteDaEventi()
torna []. In entrambi i casi o3/o4/o5 restano bloccati a 0% anche a fronte di vittorie
reali, e il fallback della route (/api/public/csi) non se ne accorgerebbe da solo: lancia un
errore solo se sia la classifica sia le partite sono vuote insieme, quindi se si rompe solo
il JSON delle partite mentre la classifica HTML continua a funzionare, la route risponde
comunque 200 con partite: [].
Come è mitigato oggi
partiteFormatoSospetto()(csi-core.ts) confronta gli eventi grezzi ricevuti con il risultato dipartiteDaEventi(): se ci sono eventi ma nessuno è stato riconosciuto come nostra partita, il formato è quasi certamente cambiato (distingue così un vero "formato rotto" da un legittimo "nessuna gara ancora in programma", dove gli eventi grezzi sono vuoti anche loro).- La route (
src/routes/api/public/csi.ts) logga unconsole.errorquando succede. - Il flag viaggia anche nella risposta JSON (
DatiCsi.formatoSospetto) fino a/classifica(src/routes/classifica.tsx), dove sostituisce la riga "Dati CSI aggiornati alle..." con un badge discreto color warning ("Il portale CSI potrebbe aver cambiato formato: dati da verificare.") — visibile a chi apre la pagina campionato, non solo nei log del server.
Come fixarlo, se succede
- Vedere il nuovo formato: guardare la risposta reale dell'endpoint, o lanciare
CSI_LIVE=1 bun test/unit/csi-core.test.ts(interroga il portale vero). - Aggiornare il parsing in
src/lib/csi-core.ts: quasi sempre basta toccarepunteggio()/parziali()(le regex sul formato del punteggio) o i nomi dei campi letti inpartiteDaEventi(). Il resto dell'app consuma solo i tipi già puliti che questo file produce (DatiCsi,PartitaCsi[]), quindi il fix resta isolato. - Serve toccare anche
src/routes/api/public/csi.tssolo se cambiano gli URL/endpoint stessi o serve autenticazione — non per un semplice cambio di formato dei dati. - Aggiornare i test:
test/unit/csi-core.test.tscon fixture nel nuovo formato, altrimenti restano verdi contro un formato che non esiste più.
Dettagli completi (endpoint, identificativi di stagione, altri limiti del collegamento CSI) in Collegamento CSI.
Copertura test
Tutti e 10 gli obiettivi hanno unit test e integration test end-to-end (dati scritti/letti da un backend reale, non solo funzione pura con contesto costruito a mano).
| Obiettivi | Unit test | Integration test |
|---|---|---|
| o1, o2, o6 | test/unit/obiettivi.test.ts |
test/integration/obiettivi.test.ts (Supabase locale, o1 anche con i convocati) |
| o7 | test/unit/obiettivi.test.ts |
test/integration/obiettivi.test.ts (Supabase locale) |
| o11 | test/unit/obiettivi.test.ts |
test/integration/obiettivi.test.ts (Supabase locale) |
| o12, o13 | test/unit/obiettivi.test.ts |
test/integration/obiettivi.test.ts (Supabase locale) |
| o3, o4, o5 | test/unit/obiettivi.test.ts |
test/integration/api.test.ts (CSI reale in produzione) |
- Presenze (o1, o7), casi limite (
test/unit/obiettivi.test.ts): solo partite e allenamenti contano, assente/forse/infortunato/nessuna risposta sono posti vuoti, il non convocato non pesa, confine di mese nel fuso Europe/Rome, convocato fuori rosa, rosa vuota, arrotondamento e soglia del 90%. Verificato anche con mutazioni (posti = tutta la rosa; eventi di squadra contati): il test diventa rosso. - Card di dettaglio (
test/unit/obiettivi-dettaglio.test.ts): ogni obiettivo ha tutti i testi, «Presenza persa» solo su o1/o7, la card renderizzata mostra tutte le sezioni e lo stato in corso/completato. - o1/o2/o6 (Supabase locale): scrive eventi e risposte veri su
eventi_app/risposte_presenze, li rilegge conleggiEventi()(la stessa funzione server dell'app) e una query REST equivalente afetchPresenze(). Copre: contesto vuoto, aggregazione su più eventi, filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico (evento dentro/fuori mese), scadenza dinamica. - o7 (Supabase locale): scrive un allenamento e una partita con convocati e presenze reali,
li rilegge con
leggiEventi()/leggiPresenze()e verifica la percentuale (il ritardo conta). - o11 (Supabase locale): scrive tre allenamenti e presenze reali, calcola
serieConsecutiva()sui dati riletti, verifica che solo chi resta in serie venga contato. - o12/o13 (Supabase locale): scrive voti veri su
pagelle_votirispettando i vincoli reali della tabella (pagelle_no_autovoto,pagelle_voto_range), li rilegge, verifica media arrotondata e conteggio. - o3/o4/o5 (CSI reale, non Supabase — le vittorie non toccano il database): estende
test/integration/api.test.ts, che già chiama/api/public/csidal vivo. Legge le vittorie vere del giorno con la stessa logica diuseObiettivi(), le passa aobiettiviSquadra()e verifica cap e target su dati reali.
Per rilanciare tutto: npm run test (unit, nessuna rete) e npm run test:integration
(richiede npx supabase start per o1/o2/o6/o7/o11/o12/o13, e rete verso CSI Bologna per
o3/o4/o5 — quest'ultimo gira comunque anche senza stack Supabase locale).