Files
CRAPP/docs/modules/obiettivi-squadra.md
davideandClaude Sonnet 5.5 e7ae2c1884 Toglie numero e data tessera dalla dashboard (DD-048); docs allineate
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>
2026-10-06 11:39:11 +02:00

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 di partiteDaEventi(): 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 un console.error quando 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

  1. 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).
  2. Aggiornare il parsing in src/lib/csi-core.ts: quasi sempre basta toccare punteggio()/parziali() (le regex sul formato del punteggio) o i nomi dei campi letti in partiteDaEventi(). Il resto dell'app consuma solo i tipi già puliti che questo file produce (DatiCsi, PartitaCsi[]), quindi il fix resta isolato.
  3. Serve toccare anche src/routes/api/public/csi.ts solo se cambiano gli URL/endpoint stessi o serve autenticazione — non per un semplice cambio di formato dei dati.
  4. Aggiornare i test: test/unit/csi-core.test.ts con 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 con leggiEventi() (la stessa funzione server dell'app) e una query REST equivalente a fetchPresenze(). 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_voti rispettando 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/csi dal vivo. Legge le vittorie vere del giorno con la stessa logica di useObiettivi(), le passa a obiettiviSquadra() 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).