2026-09-03 15:10:18 +02:00
|
|
|
|
# Modulo — Serie di presenze
|
|
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
**Stato:** implementato — tutte e tre le serie calcolate sui dati reali
|
|
|
|
|
|
**File principali:** `src/lib/serie.ts`, `src/lib/presenze.ts`, `src/lib/rosa.ts`,
|
|
|
|
|
|
`src/components/crapp/SerieCard.tsx`
|
|
|
|
|
|
**Migration collegata:** `m9_risposte_presenze_risposto_il`
|
2026-09-06 19:30:49 +02:00
|
|
|
|
**Test:** `test/unit/serie.test.ts`, `test/unit/presenze.test.ts`,
|
|
|
|
|
|
`test/integration/scritture.test.ts` (il trigger che congela `risposto_il`)
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Obiettivo
|
|
|
|
|
|
|
|
|
|
|
|
Motivare la costanza dei giocatori mostrando "serie" (streak) di comportamenti positivi
|
|
|
|
|
|
consecutivi — presenza agli allenamenti, presenza alle partite, risposta entro 24 ore alla
|
|
|
|
|
|
convocazione — con traguardi progressivi, sullo stile delle app fitness. È anche uno dei
|
|
|
|
|
|
requisiti di sblocco di alcuni [badge](badge.md) e di un [obiettivo di squadra](obiettivi-squadra.md).
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
## Le tre serie in sintesi
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
| Tipo | Campo `Giocatore` | Cosa conta | Traguardi |
|
|
|
|
|
|
| ------------- | ------------------ | ------------------------------------------------------------ | ------------ |
|
|
|
|
|
|
| `allenamenti` | `serieAllenamenti` | Allenamenti passati consecutivi con presenza | 3, 6, 10, 15 |
|
|
|
|
|
|
| `partite` | `seriePartite` | Partite passate consecutive con presenza | 2, 5, 8, 12 |
|
|
|
|
|
|
| `conferme` | `serieConferme` | Eventi consecutivi con risposta entro 24h dalla convocazione | 3, 8, 15, 20 |
|
|
|
|
|
|
|
|
|
|
|
|
Esiste un quarto contatore fuori da questo modulo, `Giocatore.streak`: la stessa regola delle
|
|
|
|
|
|
presenze ma **su partite e allenamenti insieme**. Non ha card né traguardi, compare come
|
|
|
|
|
|
"presenze consecutive" in `src/routes/index.tsx`, `src/routes/squadra.tsx` e
|
|
|
|
|
|
`src/routes/profilo.tsx`.
|
|
|
|
|
|
|
|
|
|
|
|
Le serie sono **indipendenti**: un buco agli allenamenti non tocca partite e conferme. È la
|
|
|
|
|
|
regola scritta in `aggiornaSerie()` e va mantenuta se si aggiungono altre serie.
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
## Dati
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
Non esiste una tabella delle serie e non c'è nessun contatore salvato: **le serie sono
|
|
|
|
|
|
ricalcolate da zero a ogni render**, partendo dagli eventi e dalle risposte già in cache
|
|
|
|
|
|
React Query. Nessuna query aggiuntiva, nessuna migration da rifare quando si cambia una
|
|
|
|
|
|
regola, nessun rischio di contatori disallineati dalla realtà.
|
|
|
|
|
|
|
|
|
|
|
|
Conseguenza pratica: se domani si inseriscono le presenze di eventi passati (import,
|
|
|
|
|
|
backfill, correzione a mano), le serie si aggiornano da sole al caricamento successivo.
|
|
|
|
|
|
|
|
|
|
|
|
### Tabelle lette
|
|
|
|
|
|
|
|
|
|
|
|
| Tabella | Colonne usate | A cosa servono |
|
|
|
|
|
|
| ------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
|
|
|
|
| `eventi_app` | `id`, `tipo`, `data`, `convocati`, `creato_il` | Quali impegni contano, in che ordine, e quando è partita la convocazione |
|
|
|
|
|
|
| `risposte_presenze` | `evento_id`, `giocatore_id`, `stato`, `risposto_il` | Se l'impegno è stato onorato e quanto in fretta è arrivata la risposta |
|
|
|
|
|
|
|
|
|
|
|
|
`risposto_il` (migration `m9`) è l'istante della **prima** risposta del giocatore per quell'
|
|
|
|
|
|
evento. Un trigger (`risposte_presenze_risposto_il_immutabile`) lo blocca su qualsiasi
|
|
|
|
|
|
UPDATE: senza, un giocatore che risponde subito e cambia idea una settimana dopo risulterebbe
|
|
|
|
|
|
lento. `aggiornato_il` continua a registrare l'ultima modifica ed è un'altra cosa: non usarlo
|
|
|
|
|
|
per le conferme.
|
|
|
|
|
|
|
2026-09-06 19:30:49 +02:00
|
|
|
|
Le due colonne si confondono facilmente, e sbagliarle non rompe niente di visibile: la serie
|
|
|
|
|
|
comincia solo a raccontare il falso. Per questo il confine è verificato in
|
|
|
|
|
|
`test/integration/scritture.test.ts` («la risposta di presenza si aggiorna senza far ripartire
|
|
|
|
|
|
il cronometro»), che riscrive la risposta provando a riscrivere anche `risposto_il` e controlla
|
|
|
|
|
|
che il database abbia tenuto la prima: se qualcuno togliesse il trigger, quel test diventa
|
|
|
|
|
|
rosso. Il test precedente guardava `aggiornato_il` e passava anche senza trigger.
|
|
|
|
|
|
|
|
|
|
|
|
| Colonna | Cosa registra | Chi la usa |
|
|
|
|
|
|
| --------------- | --------------------- | ----------------------- |
|
|
|
|
|
|
| `risposto_il` | la **prima** risposta | la serie "Conferme 24h" |
|
|
|
|
|
|
| `aggiornato_il` | l'**ultima** modifica | nessuna statistica |
|
|
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
Cancellare la risposta (`stato: null` → DELETE) elimina anche `risposto_il`: se il giocatore
|
|
|
|
|
|
risponde di nuovo, riparte il cronometro. È voluto — ha ritirato la risposta.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Flusso completo
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
eventi_app ─┐
|
|
|
|
|
|
├─► useEventi() ─┐
|
|
|
|
|
|
risposte_ │ (src/lib/eventi.ts) │
|
|
|
|
|
|
presenze ─┘ ├─► useRosa() ─► Giocatore.serie* ─┐
|
|
|
|
|
|
useRispostePresenze() ─┘ (rosa.ts) │
|
|
|
|
|
|
(presenze.ts) │
|
|
|
|
|
|
▼
|
|
|
|
|
|
serieGiocatore() / serieMigliore()
|
|
|
|
|
|
(serie.ts, applica serieDefs)
|
|
|
|
|
|
│
|
|
|
|
|
|
┌────────────────────────────────┼──────────────┐
|
|
|
|
|
|
▼ ▼ ▼
|
|
|
|
|
|
SerieGriglia SerieHome badges.ts
|
|
|
|
|
|
(profilo) (home) obiettivi.ts
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Chi calcola cosa:
|
|
|
|
|
|
|
|
|
|
|
|
- **`src/lib/presenze.ts`** — i tre numeri, dai dati grezzi.
|
|
|
|
|
|
- **`src/lib/rosa.ts`** — li attacca a ogni `Giocatore` dentro l'unica `useMemo` di `useRosa()`.
|
|
|
|
|
|
- **`src/lib/serie.ts`** — definizioni, traguardi, progresso e microcopy: da un numero a uno stato mostrabile.
|
|
|
|
|
|
- **`src/components/crapp/SerieCard.tsx`** — la resa a schermo.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Il calcolo (`src/lib/presenze.ts`)
|
|
|
|
|
|
|
|
|
|
|
|
Tutte le serie passano dalla stessa funzione privata `serieSu()`, che fa quattro cose in
|
|
|
|
|
|
ordine:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Filtra gli eventi rilevanti** con `eventiContanoPresenze()` — la stessa funzione che
|
|
|
|
|
|
alimenta il conteggio presenze, così le due statistiche non possono divergere:
|
|
|
|
|
|
- solo `tipo` `partita` o `allenamento` (mai `evento` o `compleanno`);
|
|
|
|
|
|
- solo eventi a cui il giocatore era convocato. **`convocati` vuoto significa "tutta la
|
|
|
|
|
|
rosa"**, non "nessuno": chi non è nell'elenco di una convocazione ristretta non vede
|
|
|
|
|
|
quell'evento e la sua serie non si spezza.
|
2026-09-06 19:30:49 +02:00
|
|
|
|
2. **Tiene solo gli eventi già passati** (`e.data < oggi`, dentro `eventiContanoPresenze()`).
|
|
|
|
|
|
Il confronto è **stretto**: l'evento di oggi non conta ancora, perché nessuno ha potuto
|
|
|
|
|
|
presentarsi e conterebbe come assenza, azzerando la serie di tutta la squadra la mattina
|
|
|
|
|
|
della partita. Entra in gioco dal giorno dopo. Il parametro `oggi` è iniettabile — di
|
|
|
|
|
|
default `dataOggi()` — e i test lo fissano a una data per non dipendere dall'orologio.
|
2026-09-04 22:46:04 +02:00
|
|
|
|
3. **Ordina per data crescente** (`localeCompare` su `YYYY-MM-DD`).
|
|
|
|
|
|
4. **Riduce** applicando `aggiornaSerie(serie, onorato(e))` a ogni evento: `+1` se onorato,
|
|
|
|
|
|
`0` altrimenti. La serie finale è quella che risulta **dopo l'ultimo evento passato**.
|
|
|
|
|
|
|
|
|
|
|
|
Quel che cambia fra le serie è solo il predicato `onorato`.
|
|
|
|
|
|
|
|
|
|
|
|
### `serieConsecutiva()` — allenamenti, partite, `streak`
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
serieConsecutiva(giocatoreId, eventi, presenze, tipo?, oggi?)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Onorato = lo stato salvato è `presente` **o** `ritardo`. Gli stati possibili sono
|
|
|
|
|
|
`presente | assente | forse | ritardo | infortunato` (`src/lib/crapp-data.ts`).
|
|
|
|
|
|
|
|
|
|
|
|
Conseguenze da conoscere prima di cambiare qualcosa:
|
|
|
|
|
|
|
2026-09-07 09:31:31 +02:00
|
|
|
|
- **`infortunato` congela la serie**: l'evento è escluso a monte (filtrato prima di
|
|
|
|
|
|
`serieSu()`), quindi non conta né come presenza né come buco — la serie resta al valore
|
|
|
|
|
|
di prima. Diverso da `contaPresenzeGiocatore()`, che continua a non contarlo come
|
|
|
|
|
|
presenza (stesso criterio `presente`/`ritardo` di prima, invariato).
|
2026-09-04 22:46:04 +02:00
|
|
|
|
- **Nessuna risposta azzera la serie.** Un evento passato per cui il giocatore non ha mai
|
|
|
|
|
|
toccato l'app equivale a un'assenza. È voluto (la serie premia anche il rispondere), ma
|
|
|
|
|
|
significa che eventi storici importati senza presenze schiacciano a zero le serie di tutti.
|
|
|
|
|
|
- Senza `tipo` conta partite e allenamenti insieme: è così che si ottiene `streak`.
|
|
|
|
|
|
|
|
|
|
|
|
### `serieConferme()` — conferme entro 24 ore
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
serieConferme(giocatoreId, eventi, tempi, oggi?)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Onorato = esiste una risposta **e** `risposto_il − creato_il ≤ 24h` (confronto inclusivo,
|
|
|
|
|
|
costante `ORE_24`, entrambi gli istanti passati da `Date.parse`).
|
|
|
|
|
|
|
|
|
|
|
|
- Conta **partite e allenamenti insieme**, non c'è una versione per tipo.
|
|
|
|
|
|
- **Lo stato non conta**: anche un "assente" dato in fretta tiene viva la serie. È una serie
|
|
|
|
|
|
sulla reattività, non sulla presenza.
|
|
|
|
|
|
- **Gli eventi senza `creatoIl` vengono saltati e non spezzano la serie.** Sono gli eventi
|
|
|
|
|
|
costruiti dal client e mai salvati a database — i compleanni di `compleanniEventi()` e la
|
|
|
|
|
|
bozza di `eventoVuoto()`. Senza istante di convocazione la domanda "ha risposto in fretta?"
|
|
|
|
|
|
non ha risposta, e trattarli come un buco punirebbe il giocatore per un dettaglio tecnico.
|
|
|
|
|
|
- **Le 24 ore partono dalla creazione dell'evento**, non da un invio di notifica: oggi un
|
|
|
|
|
|
momento di "convocazione mandata" distinto non esiste. Se un domani ci sarà, è quello
|
|
|
|
|
|
l'istante giusto da confrontare.
|
|
|
|
|
|
|
|
|
|
|
|
### Lettura e cache
|
|
|
|
|
|
|
|
|
|
|
|
`fetchPresenze()` fa **una sola query** e costruisce due mappe:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
presenze: { [eventoId]: { [giocatoreId]: Stato } }
|
|
|
|
|
|
tempi: { [eventoId]: { [giocatoreId]: string /* ISO */ } }
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Entrambe vivono nella stessa entry di React Query (`PRESENZE_KEY`, `staleTime` 5 minuti) e
|
|
|
|
|
|
`useRispostePresenze()` le espone come `presenze` e `tempi`.
|
|
|
|
|
|
|
|
|
|
|
|
`useSalvaPresenza()` non rilegge dopo la scrittura: aggiorna la cache a mano e deve tenere
|
|
|
|
|
|
allineate **entrambe** le mappe. Sull'`upsert` la colonna `risposto_il` non viene inviata —
|
|
|
|
|
|
è quello che la lascia intatta lato database sugli aggiornamenti — e la cache locale imita
|
|
|
|
|
|
la stessa regola con `istanti[giocatoreId] ??= new Date().toISOString()`: si valorizza solo
|
|
|
|
|
|
se manca. Chi tocca quella mutation deve preservare questi due dettagli, altrimenti ogni
|
|
|
|
|
|
ripensamento farebbe ripartire il cronometro delle conferme.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Da numero a card (`src/lib/serie.ts`)
|
|
|
|
|
|
|
|
|
|
|
|
`serieDefs` è l'unica fonte di verità della UI: label, descrizione, icona, traguardi e la
|
|
|
|
|
|
funzione `valore(g)` che pesca il campo giusto dal `Giocatore`.
|
|
|
|
|
|
|
|
|
|
|
|
`statoSerie(def, g)` produce quello che serve a disegnare una card:
|
|
|
|
|
|
|
|
|
|
|
|
| Campo | Come si ricava |
|
|
|
|
|
|
| ----------- | --------------------------------------------------------------------------- |
|
|
|
|
|
|
| `valore` | `def.valore(g)` |
|
|
|
|
|
|
| `prossimo` | primo traguardo **strettamente maggiore** del valore; `null` oltre l'ultimo |
|
|
|
|
|
|
| `manca` | `prossimo - valore` (`0` se fuori scala) |
|
|
|
|
|
|
| `progresso` | percentuale **dentro il livello corrente**, vedi sotto |
|
|
|
|
|
|
| `messaggio` | microcopy, vedi sotto |
|
|
|
|
|
|
|
|
|
|
|
|
### Progresso
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
progresso = round((valore - traguardoPrecedente) / (prossimo - traguardoPrecedente) * 100)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
La base è il traguardo già raggiunto, non zero. Con la vecchia formula (`valore / prossimo`)
|
|
|
|
|
|
la barra **tornava indietro** ogni volta che se ne raggiungeva uno: a 2 allenamenti segnava
|
|
|
|
|
|
67%, al terzo scendeva al 50%. Ora ogni traguardo apre un livello nuovo che riparte da 0% e
|
|
|
|
|
|
sale fino a 100%, che si tocca solo restando fuori scala (`prossimo === null`).
|
|
|
|
|
|
|
|
|
|
|
|
Esempio con i traguardi degli allenamenti (3, 6, 10, 15):
|
|
|
|
|
|
|
|
|
|
|
|
| Valore | Prossimo | Base | Progresso |
|
|
|
|
|
|
| ------ | -------- | ---- | --------- |
|
|
|
|
|
|
| 0 | 3 | 0 | 0% |
|
|
|
|
|
|
| 2 | 3 | 0 | 67% |
|
|
|
|
|
|
| 3 | 6 | 3 | 0% |
|
|
|
|
|
|
| 5 | 6 | 3 | 67% |
|
|
|
|
|
|
| 15+ | — | — | 100% |
|
|
|
|
|
|
|
|
|
|
|
|
### Messaggi
|
|
|
|
|
|
|
|
|
|
|
|
`messaggioSerie()` valuta in quest'ordine, prima corrispondenza vince:
|
|
|
|
|
|
|
|
|
|
|
|
1. `valore === 0` → «Serie … azzerata: riparti dal prossimo.»
|
|
|
|
|
|
2. `prossimo === null` → «Serie leggendaria: sei fuori scala!»
|
|
|
|
|
|
3. `manca === 1` → «Manca solo una volta al prossimo traguardo!»
|
|
|
|
|
|
4. `valore >= 5` → «Che continuità: ancora N e sali di livello.»
|
|
|
|
|
|
5. altrimenti → «Bella partenza: N al prossimo traguardo.»
|
|
|
|
|
|
|
|
|
|
|
|
Nota: il caso 1 scatta anche per chi non ha **mai** iniziato, e dice "azzerata". Se dà
|
|
|
|
|
|
fastidio, va distinto lì — il calcolo non sa differenziare "mai partito" da "appena rotto".
|
|
|
|
|
|
|
|
|
|
|
|
### Aggregatori
|
|
|
|
|
|
|
|
|
|
|
|
- `serieGiocatore(g)` — tutte le serie nell'ordine di `serieDefs`.
|
|
|
|
|
|
- `serieMigliore(g)` — quella col valore più alto. `Array.sort` è stabile, quindi **a parità
|
|
|
|
|
|
vince la prima definita in `serieDefs`**: con tutto a zero esce sempre "Allenamenti".
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Interfaccia (`src/components/crapp/SerieCard.tsx`)
|
|
|
|
|
|
|
|
|
|
|
|
- **`SerieGriglia`** — montata in `src/routes/profilo.tsx`, sezione "Serie di presenze". Una
|
|
|
|
|
|
card per serie: icona (sfondo gradiente se `valore > 0`, grigio se a zero), label,
|
|
|
|
|
|
descrizione, fiamma col numero, barra `Barra` e riga di testo `"valore/prossimo · messaggio"`
|
|
|
|
|
|
(il prefisso `valore/prossimo` sparisce fuori scala).
|
|
|
|
|
|
- **`SerieHome`** — riepilogo compatto: la serie migliore in evidenza più i tre numeri in
|
|
|
|
|
|
griglia. Attualmente **non è montata in nessuna route**: è pronta ma non usata.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Chi dipende dalle serie
|
|
|
|
|
|
|
|
|
|
|
|
Toccare la regola di calcolo muove anche questi, che non hanno logica propria:
|
|
|
|
|
|
|
|
|
|
|
|
| Dove | Cosa | Soglie |
|
|
|
|
|
|
| ------------------------------- | -------------------------------------------------------------- | --------------------------- |
|
|
|
|
|
|
| `badges.ts` `serie-allenamenti` | "Sempre in palestra", su `serieAllenamenti` | bronzo 3, argento 6, oro 10 |
|
|
|
|
|
|
| `badges.ts` `serie-conferme` | "Risposta lampo", su `serieConferme` | bronzo 3, argento 8, oro 15 |
|
|
|
|
|
|
| `badges.ts` `s-mai-forfait` | Badge segreto: `serieConferme >= 10` **e** `presenze >= 15` | — |
|
|
|
|
|
|
| `obiettivi.ts` `o11` | "Continuità di squadra": giocatori con `serieAllenamenti >= 3` | target 12 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Costo
|
|
|
|
|
|
|
|
|
|
|
|
`useRosa()` ricalcola quattro serie per ogni giocatore attivo a ogni invalidazione della
|
|
|
|
|
|
memo, e ogni serie scorre tutti gli eventi: **O(rosa × eventi)** per render memoizzato. Con
|
|
|
|
|
|
una rosa e un calendario di squadra sono numeri irrisori. Le dipendenze della memo includono
|
|
|
|
|
|
`eventi`, `mappaPresenze` e `tempi`: se in futuro qualcuna cambiasse identità a ogni render,
|
|
|
|
|
|
il costo diventerebbe per-render e andrebbe stabilizzata a monte.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Come modificare
|
|
|
|
|
|
|
|
|
|
|
|
- **Cambiare i traguardi di una serie** → l'array `traguardi` in `serieDefs`. Devono restare
|
|
|
|
|
|
crescenti (un test lo verifica) e non serve altro: progresso e messaggi si adeguano.
|
2026-09-07 09:31:31 +02:00
|
|
|
|
- **Cambiare la regola di presenza** → il predicato dentro `serieConsecutiva()`.
|
|
|
|
|
|
`infortunato` è già escluso a monte (congela la serie, non la azzera); valutare se
|
|
|
|
|
|
allineare anche `contaPresenzeGiocatore()`, che oggi conta ancora `infortunato` come
|
|
|
|
|
|
assenza ai fini statistici.
|
2026-09-04 22:46:04 +02:00
|
|
|
|
- **Non azzerare quando manca la risposta** → sempre in quel predicato: distinguere
|
|
|
|
|
|
`stato === undefined` e restituire la serie invariata invece di `false`. Richiede di
|
|
|
|
|
|
cambiare `serieSu()`, che oggi conosce solo "onorato sì/no".
|
|
|
|
|
|
- **Cambiare la finestra delle conferme** → la costante `ORE_24`.
|
|
|
|
|
|
- **Contare anche gli eventi extra-campo** (pizzate, `tipo: "evento"`) → il filtro in
|
|
|
|
|
|
`eventiContanoPresenze()`, che però è condiviso col conteggio presenze: meglio un filtro
|
|
|
|
|
|
dedicato passato a `serieSu()` che modificarlo lì.
|
|
|
|
|
|
- **Aggiungere una quarta serie** → una voce in `serieDefs` (label, descrizione, icona,
|
|
|
|
|
|
traguardi, `valore`), un campo nel tipo `Giocatore` (`crapp-data.ts`, più lo zero nel seed),
|
|
|
|
|
|
il calcolo in `presenze.ts` e il collegamento in `useRosa()`. La UI non va toccata: griglia
|
|
|
|
|
|
e home iterano su `serieDefs`.
|
|
|
|
|
|
- **Mostrare il riepilogo in home** → `SerieHome` esiste già, basta montarla.
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Limiti noti
|
|
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
**Le conferme rapide valgono solo da `m9` in avanti.** `risposto_il` non è ricostruibile a
|
|
|
|
|
|
posteriori: le righe già esistenti al momento della migration hanno ereditato `aggiornato_il`,
|
|
|
|
|
|
che è l'ultima modifica e non la prima risposta. Sui dati precedenti la serie è quindi
|
|
|
|
|
|
un'approssimazione ottimistica.
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
**Un evento passato senza risposta azzera la serie**, come un'assenza dichiarata: chi non ha
|
|
|
|
|
|
mai risposto ha serie a 0.
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
**L'ordinamento usa solo `data`, non `ora`.** Due eventi lo stesso giorno vengono processati
|
|
|
|
|
|
nell'ordine in cui arrivano dalla query (`.order("data")`), quindi non deterministico fra
|
|
|
|
|
|
loro. Irrilevante finché un buco e una presenza nello stesso giorno danno lo stesso
|
|
|
|
|
|
risultato finale, ma va sistemato se un giorno serve l'ordine esatto.
|
|
|
|
|
|
|
2026-09-07 09:47:02 +02:00
|
|
|
|
**`oggi` è sempre in fuso Italia.** `dataOggi()` (`src/lib/scout-live.ts`) usa
|
|
|
|
|
|
`Intl.DateTimeFormat` con `timeZone: "Europe/Rome"`, non i getter locali di `Date` né
|
|
|
|
|
|
`toISOString()`: il cambio ora legale/solare lo gestisce il database IANA dei fusi, non un
|
|
|
|
|
|
offset scritto a mano. È lo stesso `oggi` di `serieConsecutiva()`, `serieConferme()` e del
|
|
|
|
|
|
conteggio presenze — prima `serieConsecutiva()`/`serieConferme()` calcolavano `oggi` con
|
|
|
|
|
|
`toISOString()` (sempre UTC) mentre il conteggio presenze usava i getter locali di `Date`
|
|
|
|
|
|
(corretti solo se il processo gira già in fuso italiano): nelle prime ore della giornata
|
|
|
|
|
|
italiana potevano non essere d'accordo su cosa fosse "oggi".
|
2026-09-03 15:10:18 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Evoluzioni possibili
|
|
|
|
|
|
|
2026-09-04 22:46:04 +02:00
|
|
|
|
- Istante di convocazione esplicito (invio notifica) da usare al posto di `creato_il` per le
|
|
|
|
|
|
conferme.
|
|
|
|
|
|
- Distinguere "serie mai iniziata" da "serie interrotta" nel microcopy.
|
|
|
|
|
|
- Verificare che i badge e l'obiettivo "Continuità di squadra" si sblocchino davvero sui dati
|
|
|
|
|
|
di stagione.
|