2 Commits
Author SHA1 Message Date
davideandClaude Sonnet 5.5 daa0dd9c60 Porta la versione a 1.3.1: «Prossimi compleanni» nel Calendario
La sezione in fondo al Calendario mostra solo i compleanni del mese in corso non
ancora passati. Sezione 1.3.1 del CHANGELOG, versione in package.json e
package-lock.json, PROJECT_STATE e modulo calendario aggiornati. Nessuna migration.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 13:29:08 +02:00
davideandClaude Sonnet 5.5 bfb768fb5a Rimuove la serie «Conferme 24h» e i badge collegati
Toglie la serie dal profilo, dalla Home e dalle notifiche di traguardo, insieme
ai badge «Risposta lampo» e «Mai un forfait» e al calcolo di serieConferme.
La colonna risposto_il e il suo trigger restano nel database, inutilizzati.
Test e documentazione allineati.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 13:21:14 +02:00
22 changed files with 105 additions and 729 deletions

No files matched your search

+4 -3
View File
@@ -9,9 +9,10 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
## Stato generale ## Stato generale
- **Versione:** 1.3.0 (tag `v1.3.0` su `main`) — notifica di compleanno, obiettivi cliccabili con card di - **Versione:** 1.3.1 (in produzione su `main` la 1.3.0, tag `v1.3.0`) — il Calendario mostra solo i prossimi
dettaglio, presenze calcolate sui convocati, avvisi in Home ridisegnati, caratteri più grandi, tessera CSI compleanni del mese; tolta la serie «Conferme 24h» con i badge «Risposta lampo» e «Mai un forfait». La 1.3.0
tolta dalla dashboard. Dettaglio in [docs/CHANGELOG.md](docs/CHANGELOG.md). portava la notifica di compleanno, gli obiettivi cliccabili, le presenze sui convocati, gli avvisi in Home
ridisegnati e i caratteri più grandi. Dettaglio in [docs/CHANGELOG.md](docs/CHANGELOG.md).
- **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio - **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio
Project Ref `hetycilxgkdmccelwerq` non si usano più. Project Ref `hetycilxgkdmccelwerq` non si usano più.
- **Migration:** 42 file in `supabase/migrations/`, fino a `m31_notifica_compleanno`. Le prime 41 (fino a - **Migration:** 42 file in `supabase/migrations/`, fino a `m31_notifica_compleanno`. Le prime 41 (fino a
+17
View File
@@ -13,6 +13,23 @@ versioni `0.y.z` erano pre-release.
## [Non rilasciato] ## [Non rilasciato]
## [1.3.1] - 2026-10-06
Il Calendario mostra solo i prossimi compleanni del mese, e le serie di presenze sono due invece di tre: sparisce
«Conferme 24h» con i due badge che ne dipendevano. Nessuna migration da applicare.
### Modificato
- **Calendario: «Prossimi compleanni»** — la sezione in fondo mostra solo i compleanni del mese in corso non
ancora passati, non più quelli del mese selezionato nella griglia; se nel mese non ce ne sono, lo dice
(«Nessun compleanno questo mese»).
### Rimosso
- **Serie «Conferme 24h» e badge collegati** — la serie non compare più nel profilo, in Home né nelle notifiche di
traguardo; restano Allenamenti e Partite. Rimossi anche il badge «Risposta lampo» e il segreto «Mai un forfait», che
si basavano su di essa. Nessuna migrazione: la colonna `risposto_il` resta, inutilizzata.
## [1.3.0] - 2026-10-06 ## [1.3.0] - 2026-10-06
Gli auguri di compleanno arrivano da soli, gli obiettivi di squadra si possono toccare per capire come funzionano Gli auguri di compleanno arrivano da soli, gli obiettivi di squadra si possono toccare per capire come funzionano
+1 -1
View File
@@ -77,7 +77,7 @@ Colonne di `giocatori_squadra` che meritano una nota:
| Tabella | Scopo | Note | | Tabella | Scopo | Note |
| ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventi_app` | Eventi gestionali usati dall'app. | Modello in uso. Cancellare un evento pulisce a cascata, via trigger, tutte le tabelle collegate: `risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` (DD-029). `bonifica_dati_evento_orfani()` (solo service role) ripulisce eventuali righe orfane. | | `eventi_app` | Eventi gestionali usati dall'app. | Modello in uso. Cancellare un evento pulisce a cascata, via trigger, tutte le tabelle collegate: `risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` (DD-029). `bonifica_dati_evento_orfani()` (solo service role) ripulisce eventuali righe orfane. |
| `risposte_presenze` | Risposte dei giocatori agli eventi. | `risposto_il` è l'istante della **prima** risposta e un trigger lo rende immutabile: confrontato con `eventi_app.creato_il` dà la serie «Conferme 24h». `aggiornato_il` è l'ultima modifica. | | `risposte_presenze` | Risposte dei giocatori agli eventi. | `risposto_il` è l'istante della **prima** risposta e un trigger lo rende immutabile: oggi nessuna statistica lo usa (la serie «Conferme 24h» è stata rimossa il 2026-10-06). `aggiornato_il` è l'ultima modifica. |
| `cacche_partita` | Sondaggio prepartita. | Usato per statistiche e badge segreti. | | `cacche_partita` | Sondaggio prepartita. | Usato per statistiche e badge segreti. |
| `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello «nuovo» con autenticazione e vincoli, non adottato (DD-014). | | `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello «nuovo» con autenticazione e vincoli, non adottato (DD-014). |
| `presenze` | Presenze agli eventi. | Come sopra (DD-014). | | `presenze` | Presenze agli eventi. | Come sopra (DD-014). |
+1 -1
View File
@@ -12,7 +12,7 @@ il campionato. Dove serve il dettaglio c'è il rimando al modulo.
| **Evento** | Riga di `eventi_app`: `partita`, `allenamento` o `evento` extra-campo (es. cena). I compleanni compaiono in calendario ma non sono eventi di campo. | | **Evento** | Riga di `eventi_app`: `partita`, `allenamento` o `evento` extra-campo (es. cena). I compleanni compaiono in calendario ma non sono eventi di campo. |
| **Convocati** | Giocatori chiamati a un evento. **`convocati` vuoto = tutta la rosa attiva**: è la convenzione di `convocatiEvento()` e di `evento_permette_voto()`. | | **Convocati** | Giocatori chiamati a un evento. **`convocati` vuoto = tutta la rosa attiva**: è la convenzione di `convocatiEvento()` e di `evento_permette_voto()`. |
| **Presenza / risposta** | Risposta di un giocatore a un evento (`risposte_presenze`): `presente`, `ritardo`, `assente`, `forse`, `infortunato`. Vedi [modules/presenze.md](modules/presenze.md). | | **Presenza / risposta** | Risposta di un giocatore a un evento (`risposte_presenze`): `presente`, `ritardo`, `assente`, `forse`, `infortunato`. Vedi [modules/presenze.md](modules/presenze.md). |
| **Serie** | Streak di comportamenti positivi consecutivi (allenamenti, partite, conferme entro 24 ore). Vedi [modules/serie-presenze.md](modules/serie-presenze.md). | | **Serie** | Streak di comportamenti positivi consecutivi (allenamenti e partite). Vedi [modules/serie-presenze.md](modules/serie-presenze.md). |
| **Pagelle** | Voti 1-10 anonimi ai compagni dopo una partita; solo i convocati votano, finché `pagelle_chiuse = false` (DD-027). Vedi [modules/pagelle.md](modules/pagelle.md). | | **Pagelle** | Voti 1-10 anonimi ai compagni dopo una partita; solo i convocati votano, finché `pagelle_chiuse = false` (DD-027). Vedi [modules/pagelle.md](modules/pagelle.md). |
| **MVP** | Voto al migliore in campo a fine partita, uno per votante (DD-027). Vedi [modules/mvp.md](modules/mvp.md). | | **MVP** | Voto al migliore in campo a fine partita, uno per votante (DD-027). Vedi [modules/mvp.md](modules/mvp.md). |
| **Badge** | Riconoscimenti calcolati a runtime, mai salvati (DD-007): normali (bronzo/argento/oro), segreti, social (votati dai compagni). Vedi [modules/badge.md](modules/badge.md). | | **Badge** | Riconoscimenti calcolati a runtime, mai salvati (DD-007): normali (bronzo/argento/oro), segreti, social (votati dai compagni). Vedi [modules/badge.md](modules/badge.md). |
+28 -34
View File
@@ -76,7 +76,7 @@ badge assegnati per voto dai compagni.
## Elenco badge ## Elenco badge
Riferimento completo per chi lavora sul codice. **In app i 5 badge segreti restano nascosti Riferimento completo per chi lavora sul codice. **In app i 4 badge segreti restano nascosti
finché non sbloccati** (fanno parte della sorpresa per i giocatori): elencarli qui, con le finché non sbloccati** (fanno parte della sorpresa per i giocatori): elencarli qui, con le
condizioni esatte, è una scelta deliberata per la documentazione tecnica, non una fuga di condizioni esatte, è una scelta deliberata per la documentazione tecnica, non una fuga di
informazioni verso l'interfaccia. informazioni verso l'interfaccia.
@@ -93,7 +93,6 @@ soglia raggiunta o superata (soglie inclusive), oltre l'oro resta oro.
| `palloni` | Sherpa dei palloni | quante volte hai confermato il turno palloni (`g.palloni`) — le proposte automatiche non ancora confermate non contano | 3 / 6 / 10 | | `palloni` | Sherpa dei palloni | quante volte hai confermato il turno palloni (`g.palloni`) — le proposte automatiche non ancora confermate non contano | 3 / 6 / 10 |
| `presenze` | Presenza fissa | totale presenze (presente o ritardo) a eventi/partite di sempre, non solo della stagione in corso (`g.presenze`) | 5 / 15 / 30 | | `presenze` | Presenza fissa | totale presenze (presente o ritardo) a eventi/partite di sempre, non solo della stagione in corso (`g.presenze`) | 5 / 15 / 30 |
| `serie-allenamenti` | Sempre in palestra | allenamenti consecutivi presenti (`g.serieAllenamenti`); un infortunio non spezza la serie, un'assenza sì | 3 / 6 / 10 | | `serie-allenamenti` | Sempre in palestra | allenamenti consecutivi presenti (`g.serieAllenamenti`); un infortunio non spezza la serie, un'assenza sì | 3 / 6 / 10 |
| `serie-conferme` | Risposta lampo | conferme di presenza consecutive date entro 24h dalla convocazione (`g.serieConferme`) | 3 / 8 / 15 |
### Badge segreti (booleani, nascosti finché non sbloccati) ### Badge segreti (booleani, nascosti finché non sbloccati)
@@ -104,13 +103,12 @@ grado effettivo che risulta una volta sbloccato è sempre **`"oro"`** (l'ultimo
`for` sovrascrive), mai `"bronzo"` — l'unica cosa che conta davvero per questi badge è `for` sovrascrive), mai `"bronzo"` — l'unica cosa che conta davvero per questi badge è
`grado !== null`, non il suo valore, ed è così che li legge `badgeSegretiSbloccati()`. `grado !== null`, non il suo valore, ed è così che li legge `badgeSegretiSbloccati()`.
| id | nome | condizione esatta | | id | nome | condizione esatta |
| --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | -------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `s-tiebreak` | Uomo tie-break | almeno 2 MVP **e** media pagella ≥ 8, sopra la soglia minima di voti di Pagellone (`g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8`) | | `s-tiebreak` | Uomo tie-break | almeno 2 MVP **e** media pagella ≥ 8, sopra la soglia minima di voti di Pagellone (`g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8`) |
| `s-mai-forfait` | Mai un forfait | almeno 10 conferme rapide consecutive **e** almeno 15 presenze (`g.serieConferme >= 10 && g.presenze >= 15`) | | `s-infermeria` | Cliente VIP dell'Infermeria | almeno 3 eventi saltati per infortunio (`g.infortuni >= 3`) |
| `s-infermeria` | Cliente VIP dell'Infermeria | almeno 3 eventi saltati per infortunio (`g.infortuni >= 3`) | | `s-ritardi` | Aspettate, arrivo! | almeno 5 ritardi a eventi (`g.ritardi >= 5`) |
| `s-ritardi` | Aspettate, arrivo! | almeno 5 ritardi a eventi (`g.ritardi >= 5`) | | `s-cacche` | Trono di ferro | almeno 3 partite (campionato o amichevole) con 3 o più cacche pre-gara dichiarate (`g.cacche >= 3`) |
| `s-cacche` | Trono di ferro | almeno 3 partite (campionato o amichevole) con 3 o più cacche pre-gara dichiarate (`g.cacche >= 3`) |
### Badge social (votati dai compagni, 5 categorie per partita) ### Badge social (votati dai compagni, 5 categorie per partita)
@@ -149,24 +147,22 @@ dicono i file, non questa pagina: leggili prima di cambiare una soglia.
### Riepilogo per badge ### Riepilogo per badge
| # | id | tipo | test unit | test integration | | # | id | tipo | test unit | test integration |
| --- | ------------------- | ------- | ----------------------------- | --------------------------------------------- | | --- | ------------------- | ------------ | ----------------------------- | --------------------------------------------- |
| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) | | 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) |
| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) | | 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) |
| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) | | 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) |
| 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) | | 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) |
| 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) | | 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) |
| 6 | `serie-conferme` | normale | ✅ (vedi Limiti noti) | ✅ (`serie-conferme-badge`) | | 6 | 7 | `s-tiebreak` | segreto | ✅ (soglia minima voti) | ✅ (`s-tiebreak-badge`) |
| 7 | `s-tiebreak` | segreto | ✅ (soglia minima voti) | ✅ (`s-tiebreak-badge`) | | 6 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) |
| 8 | `s-mai-forfait` | segreto | ✅ | ✅ (`s-mai-forfait-badge`) | | 7 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) |
| 9 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) | | 8 | `s-cacche` | segreto | ✅ | ✅ (`s-cacche-badge`) |
| 10 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) | | 9 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) |
| 11 | `s-cacche` | segreto | ✅ | ✅ (`s-cacche-badge`) | | 10 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) |
| 12 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) | | 11 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) |
| 13 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | | 12 | `meme` | social | ✅ | ✅ (`badge-social`) |
| 14 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | | 13 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) |
| 15 | `meme` | social | ✅ | ✅ (`badge-social`) |
| 16 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) |
--- ---
@@ -193,11 +189,9 @@ dicono i file, non questa pagina: leggili prima di cambiare una soglia.
## Limiti noti ## Limiti noti
- **Dipendenza dal modulo [Serie](serie-presenze.md)**: i badge "Sempre in palestra", - **Dipendenza dal modulo [Serie](serie-presenze.md)**: il badge «Sempre in palestra» si muove solo se cambia la serie
"Risposta lampo" e il segreto "Mai un forfait" si muovono solo se cambiano le serie. Le Allenamenti. Il badge «Risposta lampo» e il segreto «Mai un forfait», basati sulla serie «Conferme 24h», sono stati
serie sono calcolate sui dati reali dalla migration `m9` in avanti, ma "Risposta lampo" e rimossi il 2026-10-06.
"Mai un forfait" dipendono da `serieConferme`, e `risposto_il` non è ricostruibile per le
risposte precedenti a `m9`: su quelle righe la serie è un'approssimazione.
- Nessuno storico dei badge sbloccati: se cambiano le soglie o i dati sorgente, un badge già - Nessuno storico dei badge sbloccati: se cambiano le soglie o i dati sorgente, un badge già
"ottenuto" può sparire o apparire retroattivamente. "ottenuto" può sparire o apparire retroattivamente.
- Notifiche "nuovo badge" solo locali al dispositivo (localStorage), si ripetono cambiando - Notifiche "nuovo badge" solo locali al dispositivo (localStorage), si ripetono cambiando
@@ -213,7 +207,7 @@ UI di rotazione (`TurnoPalloni.tsx`, `PromemoriaPalloni.tsx`), mai per il conteg
Dimostrato con dati veri in `palloni-badge.test.ts`. [palloni.md](palloni.md) aggiornato di Dimostrato con dati veri in `palloni-badge.test.ts`. [palloni.md](palloni.md) aggiornato di
conseguenza. conseguenza.
**Risolto (audit completo dei 16 badge)**: `s-tiebreak` (`badges.ts:139`) usava `g.mediaVoto` **Risolto (audit completo dei badge (all'epoca 16))**: `s-tiebreak` (`badges.ts:139`) usava `g.mediaVoto`
senza applicare `VOTI_MINIMI_PAGELLA`, a differenza del badge normale `pagella` che usa lo senza applicare `VOTI_MINIMI_PAGELLA`, a differenza del badge normale `pagella` che usa lo
stesso campo — un giocatore con un solo voto pagella altissimo e 2 MVP poteva sbloccare il stesso campo — un giocatore con un solo voto pagella altissimo e 2 MVP poteva sbloccare il
segreto senza che la media fosse statisticamente significativa. Ora `s-tiebreak` richiede anche segreto senza che la media fosse statisticamente significativa. Ora `s-tiebreak` richiede anche
@@ -238,7 +232,7 @@ ancora correggere un voto anche fuori convocazione o dopo la chiusura.
## Evoluzioni possibili ## Evoluzioni possibili
- Sincronizzare lo stato "visto" su Supabase invece che solo in localStorage. - Sincronizzare lo stato "visto" su Supabase invece che solo in localStorage.
- Verificare sui dati di stagione che i tre badge legati alle serie si sblocchino davvero, - Verificare sui dati di stagione che il badge legato alla serie si sblocchino davvero,
ora che le serie sono calcolate. ora che le serie sono calcolate.
- Rimuovere `badgeSbloccati()` (codice morto) o documentarne lo scopo. - Rimuovere `badgeSbloccati()` (codice morto) o documentarne lo scopo.
- Aggiungere un vincolo (CHECK o FK) sulla colonna `categoria` di `badge_social_voti`. - Aggiungere un vincolo (CHECK o FK) sulla colonna `categoria` di `badge_social_voti`.
+5 -1
View File
@@ -24,7 +24,11 @@ scout e turno palloni — la maggior parte degli altri moduli dipende da un `eve
in `eventi.ts`, generati a runtime dall'anagrafica di `useAnagraficaRosa({ conAllenatori: in `eventi.ts`, generati a runtime dall'anagrafica di `useAnagraficaRosa({ conAllenatori:
true })` — ci sono anche gli allenatori, DD-034 — non righe true })` — ci sono anche gli allenatori, DD-034 — non righe
vere di `eventi_app`): la spunta della vista `giorniIT`/`mesiIT` colora la cella per tipo vere di `eventi_app`): la spunta della vista `giorniIT`/`mesiIT` colora la cella per tipo
di evento, i giorni con più eventi si dividono lo spazio. di evento, i giorni con più eventi si dividono lo spazio. In fondo alla pagina la sezione
«Prossimi compleanni» elenca solo quelli del **mese in corso** non ancora passati (oggi
compreso), a prescindere dal mese mostrato nella griglia: il 6 ottobre un compleanno del 3
non compare più. Le celle della griglia continuano a mostrare tutti i compleanni del mese
selezionato.
- **`/eventi`** — "Gestione eventi", riservata ad amministratori e allenatori - **`/eventi`** — "Gestione eventi", riservata ad amministratori e allenatori
(`usePuoGestireEventi()`, [DD-034](../decisions/DD-034.md)): (`usePuoGestireEventi()`, [DD-034](../decisions/DD-034.md)):
crea, modifica ed elimina un evento, sceglie i convocati tra i soli giocatori crea, modifica ed elimina un evento, sceglie i convocati tra i soli giocatori
+18 -37
View File
@@ -1,12 +1,12 @@
# Modulo — Serie di presenze # Modulo — Serie di presenze
**Stato:** implementato — tutte e tre le serie calcolate sui dati reali **Stato:** implementato — le due serie (allenamenti e partite) calcolate sui dati reali
**File principali:** `src/lib/serie.ts`, `src/lib/presenze.ts`, `src/lib/rosa.ts`, **File principali:** `src/lib/serie.ts`, `src/lib/presenze.ts`, `src/lib/rosa.ts`,
`src/components/crapp/SerieCard.tsx` `src/components/crapp/SerieCard.tsx`
**Migration collegata:** `m9_risposte_presenze_risposto_il` **Migration collegata:** `m9_risposte_presenze_risposto_il`
**Test:** `test/unit/serie.test.ts`, `test/unit/presenze.test.ts`, **Test:** `test/unit/serie.test.ts`, `test/unit/presenze.test.ts`,
`test/integration/scritture.test.ts` (il trigger che congela `risposto_il`), `test/integration/scritture.test.ts` (il trigger che congela `risposto_il`),
`test/integration/serie-allenamenti-badge.test.ts`, `test/integration/serie-conferme-badge.test.ts` `test/integration/serie-allenamenti-badge.test.ts`
--- ---
@@ -19,15 +19,18 @@ requisiti di sblocco di alcuni [badge](badge.md) e di un [obiettivo di squadra](
--- ---
## Le tre serie in sintesi ## Le due serie in sintesi
| Tipo | Campo `Giocatore` | Cosa conta | Traguardi | | Tipo | Campo `Giocatore` | Cosa conta | Traguardi |
| ------------- | ------------------ | ------------------------------------------------------------ | ------------ | | ------------- | ------------------ | -------------------------------------------- | ------------ |
| `allenamenti` | `serieAllenamenti` | Allenamenti passati consecutivi con presenza | 3, 6, 10, 15 | | `allenamenti` | `serieAllenamenti` | Allenamenti passati consecutivi con presenza | 3, 6, 10, 15 |
| `partite` | `seriePartite` | Partite passate consecutive con presenza | 2, 5, 8, 12 | | `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 La serie «Conferme 24h», con i badge «Risposta lampo» e «Mai un forfait» che ne dipendevano, è stata rimossa il
2026-10-06: restano `Giocatore.streak` e le due serie sopra. La colonna `risposto_il` e il trigger che la congela
restano nel database, ma oggi nessuna statistica li usa.
Esiste un terzo 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 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 "presenze consecutive" in `src/routes/index.tsx`, `src/routes/squadra.tsx` e
`src/routes/profilo.tsx`. `src/routes/profilo.tsx`.
@@ -67,10 +70,10 @@ il cronometro»), che riscrive la risposta provando a riscrivere anche `risposto
che il database abbia tenuto la prima: se qualcuno togliesse il trigger, quel test diventa 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. rosso. Il test precedente guardava `aggiornato_il` e passava anche senza trigger.
| Colonna | Cosa registra | Chi la usa | | Colonna | Cosa registra | Chi la usa |
| --------------- | --------------------- | ----------------------- | | --------------- | --------------------- | -------------------------------------- |
| `risposto_il` | la **prima** risposta | la serie "Conferme 24h" | | `risposto_il` | la **prima** risposta | nessuna statistica (ex «Conferme 24h») |
| `aggiornato_il` | l'**ultima** modifica | nessuna statistica | | `aggiornato_il` | l'**ultima** modifica | nessuna statistica |
Cancellare la risposta (`stato: null` → DELETE) elimina anche `risposto_il`: se il giocatore Cancellare la risposta (`stato: null` → DELETE) elimina anche `risposto_il`: se il giocatore
risponde di nuovo, riparte il cronometro. È voluto — ha ritirato la risposta. risponde di nuovo, riparte il cronometro. È voluto — ha ritirato la risposta.
@@ -147,26 +150,6 @@ Conseguenze da conoscere prima di cambiare qualcosa:
significa che eventi storici importati senza presenze schiacciano a zero le serie di tutti. 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`. - 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 ### Lettura e cache
`fetchPresenze()` fa **una sola query** e costruisce due mappe: `fetchPresenze()` fa **una sola query** e costruisce due mappe:
@@ -263,8 +246,6 @@ Toccare la regola di calcolo muove anche questi, che non hanno logica propria:
| Dove | Cosa | Soglie | | Dove | Cosa | Soglie |
| ------------------------------- | -------------------------------------------------------------- | --------------------------- | | ------------------------------- | -------------------------------------------------------------- | --------------------------- |
| `badges.ts` `serie-allenamenti` | "Sempre in palestra", su `serieAllenamenti` | bronzo 3, argento 6, oro 10 | | `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 | | `obiettivi.ts` `o11` | "Continuità di squadra": giocatori con `serieAllenamenti >= 3` | target 12 |
--- ---
@@ -320,8 +301,8 @@ risultato finale, ma va sistemato se un giorno serve l'ordine esatto.
**`oggi` è sempre in fuso Italia.** `dataOggi()` (`src/lib/scout-live.ts`) usa **`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é `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 `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 offset scritto a mano. È lo stesso `oggi` di `serieConsecutiva()` e del
conteggio presenze — prima `serieConsecutiva()`/`serieConferme()` calcolavano `oggi` con conteggio presenze — prima `serieConsecutiva()` e `serieConferme()` (poi rimossa) calcolavano `oggi` con
`toISOString()` (sempre UTC) mentre il conteggio presenze usava i getter locali di `Date` `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 (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". italiana potevano non essere d'accordo su cosa fosse "oggi".
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "crapp", "name": "crapp",
"version": "1.3.0", "version": "1.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "crapp", "name": "crapp",
"version": "1.3.0", "version": "1.3.1",
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.111.0", "@supabase/supabase-js": "^2.111.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "crapp", "name": "crapp",
"version": "1.3.0", "version": "1.3.1",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"type": "module", "type": "module",
-23
View File
@@ -3,9 +3,7 @@ import {
Repeat, Repeat,
Lock, Lock,
Ghost, Ghost,
Zap,
Rocket, Rocket,
Anchor,
Stethoscope, Stethoscope,
AlarmClock, AlarmClock,
ClipboardCheck, ClipboardCheck,
@@ -114,15 +112,6 @@ export const badgeDefs: BadgeDef[] = [
soglie: { bronzo: 3, argento: 6, oro: 10 }, soglie: { bronzo: 3, argento: 6, oro: 10 },
valore: (g) => g.serieAllenamenti, valore: (g) => g.serieAllenamenti,
}, },
{
id: "serie-conferme",
nome: "Risposta lampo",
descrizione: "Conferme rapide agli eventi: la squadra sa subito che ci sei.",
unita: "conferme entro 24h",
icon: Zap,
soglie: { bronzo: 3, argento: 8, oro: 15 },
valore: (g) => g.serieConferme,
},
]; ];
/** Badge segreti: non compaiono nella UI finché non vengono sbloccati. */ /** Badge segreti: non compaiono nella UI finché non vengono sbloccati. */
@@ -141,18 +130,6 @@ export const badgeSegreti: BadgeDef[] = [
valore: (g) => (g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8 ? 1 : 0), valore: (g) => (g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8 ? 1 : 0),
celebrazione: "Nei momenti caldi ci sei sempre.", celebrazione: "Nei momenti caldi ci sei sempre.",
}, },
{
id: "s-mai-forfait",
nome: "Mai un forfait",
descrizione:
"Sbloccato con 10 conferme rapide consecutive e 15 presenze: su di te la squadra può contare a occhi chiusi.",
unita: "requisito nascosto",
icon: Anchor,
segreto: true,
soglie: { bronzo: 1, argento: 1, oro: 1 },
valore: (g) => (g.serieConferme >= 10 && g.presenze >= 15 ? 1 : 0),
celebrazione: "Su di te la squadra può contare a occhi chiusi.",
},
{ {
id: "s-infermeria", id: "s-infermeria",
nome: "Cliente VIP dell'Infermeria", nome: "Cliente VIP dell'Infermeria",
-2
View File
@@ -34,7 +34,6 @@ export type Giocatore = {
/** Serie consecutive per tipo: si azzerano in modo indipendente. */ /** Serie consecutive per tipo: si azzerano in modo indipendente. */
serieAllenamenti: number; serieAllenamenti: number;
seriePartite: number; seriePartite: number;
serieConferme: number;
/** MVP eletti dalla squadra. */ /** MVP eletti dalla squadra. */
mvp: number; mvp: number;
/** Media delle pagelle ricevute dai compagni (1-10). */ /** Media delle pagelle ricevute dai compagni (1-10). */
@@ -109,7 +108,6 @@ export const giocatori: Giocatore[] = rosaCSI
streak: 0, streak: 0,
serieAllenamenti: 0, serieAllenamenti: 0,
seriePartite: 0, seriePartite: 0,
serieConferme: 0,
mvp: 0, mvp: 0,
mediaVoto: 0, mediaVoto: 0,
votiPagella: 0, votiPagella: 0,
+1 -26
View File
@@ -155,31 +155,6 @@ export function serieConsecutiva(
); );
} }
const ORE_24 = 24 * 60 * 60 * 1000;
/**
* Serie di conferme rapide: risposte arrivate entro 24 ore dalla convocazione
* (`creatoIl` dell'evento). Gli eventi senza istante di creazione — quelli generati
* dal client, non salvati a database — non spezzano la serie: vengono saltati.
*/
export function serieConferme(
giocatoreId: string,
eventi: Evento[],
tempi: MappaTempiRisposta,
oggi: string = dataOggi(),
): number {
return serieSu(
giocatoreId,
eventi.filter((e) => e.creatoIl),
oggi,
undefined,
(e) => {
const risposto = tempi[e.id]?.[giocatoreId];
return risposto !== undefined && Date.parse(risposto) - Date.parse(e.creatoIl!) <= ORE_24;
},
);
}
/** Scorre gli eventi già passati in ordine di data applicando la regola delle serie. */ /** Scorre gli eventi già passati in ordine di data applicando la regola delle serie. */
function serieSu( function serieSu(
giocatoreId: string, giocatoreId: string,
@@ -228,7 +203,7 @@ export function usePresenzeEvento(eventoId: string) {
* *
* - l'istante si scrive **solo se manca** (`??=`), come fa il database, dove `risposto_il` * - l'istante si scrive **solo se manca** (`??=`), come fa il database, dove `risposto_il`
* non viene inviato sull'upsert e un trigger lo congela: è la prima risposta, non l'ultima, * non viene inviato sull'upsert e un trigger lo congela: è la prima risposta, non l'ultima,
* e un ripensamento non deve far ripartire il cronometro della serie "Conferme 24h"; * e un ripensamento non deve cambiare l'istante della prima risposta;
* - cancellare la risposta (`stato: null`) elimina **anche** l'istante, così se il giocatore * - cancellare la risposta (`stato: null`) elimina **anche** l'istante, così se il giocatore
* risponde di nuovo il cronometro riparte davvero — ha ritirato la risposta. * risponde di nuovo il cronometro riparte davvero — ha ritirato la risposta.
*/ */
+2 -15
View File
@@ -12,7 +12,6 @@ import { useEventi } from "./eventi";
import { import {
contaPartiteGiocate, contaPartiteGiocate,
contaPresenzeGiocatore, contaPresenzeGiocatore,
serieConferme,
serieConsecutiva, serieConsecutiva,
totaliEventiGiocatore, totaliEventiGiocatore,
useRispostePresenze, useRispostePresenze,
@@ -68,7 +67,7 @@ export function useRosa(): Giocatore[] {
const { salvati: turniSalvati } = useTurniPalloni(); const { salvati: turniSalvati } = useTurniPalloni();
const { infortuni, ritardi } = useInfortuniERitardi(); const { infortuni, ritardi } = useInfortuniERitardi();
const { eventi } = useEventi(); const { eventi } = useEventi();
const { presenze: mappaPresenze, tempi } = useRispostePresenze(); const { presenze: mappaPresenze } = useRispostePresenze();
const votiMvp = voti.data; const votiMvp = voti.data;
@@ -94,7 +93,6 @@ export function useRosa(): Giocatore[] {
streak: serieConsecutiva(g.id, eventi, mappaPresenze), streak: serieConsecutiva(g.id, eventi, mappaPresenze),
serieAllenamenti: serieConsecutiva(g.id, eventi, mappaPresenze, "allenamento"), serieAllenamenti: serieConsecutiva(g.id, eventi, mappaPresenze, "allenamento"),
seriePartite: serieConsecutiva(g.id, eventi, mappaPresenze, "partita"), seriePartite: serieConsecutiva(g.id, eventi, mappaPresenze, "partita"),
serieConferme: serieConferme(g.id, eventi, tempi),
mvp: mvpVinti[g.id] ?? 0, mvp: mvpVinti[g.id] ?? 0,
mediaVoto: medie[g.id]?.media ?? 0, mediaVoto: medie[g.id]?.media ?? 0,
votiPagella: medie[g.id]?.voti ?? 0, votiPagella: medie[g.id]?.voti ?? 0,
@@ -105,18 +103,7 @@ export function useRosa(): Giocatore[] {
infortuni: infortuni[g.id] ?? 0, infortuni: infortuni[g.id] ?? 0,
ritardi: ritardi[g.id] ?? 0, ritardi: ritardi[g.id] ?? 0,
})); }));
}, [ }, [squadra, votiMvp, pagelle, cacche, turniSalvati, infortuni, ritardi, eventi, mappaPresenze]);
squadra,
votiMvp,
pagelle,
cacche,
turniSalvati,
infortuni,
ritardi,
eventi,
mappaPresenze,
tempi,
]);
} }
/** Criteri di ordinamento della classifica interna di Squadra. */ /** Criteri di ordinamento della classifica interna di Squadra. */
+2 -10
View File
@@ -1,8 +1,8 @@
import { Dumbbell, Swords, Zap } from "lucide-react"; import { Dumbbell, Swords } from "lucide-react";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import type { Giocatore } from "./crapp-data"; import type { Giocatore } from "./crapp-data";
export type SerieTipo = "allenamenti" | "partite" | "conferme"; export type SerieTipo = "allenamenti" | "partite";
export type SerieDef = { export type SerieDef = {
tipo: SerieTipo; tipo: SerieTipo;
@@ -31,14 +31,6 @@ export const serieDefs: SerieDef[] = [
traguardi: [2, 5, 8, 12], traguardi: [2, 5, 8, 12],
valore: (g) => g.seriePartite, valore: (g) => g.seriePartite,
}, },
{
tipo: "conferme",
label: "Conferme 24h",
descrizione: "Risposte date entro 24 ore dalla convocazione",
icon: Zap,
traguardi: [3, 8, 15, 20],
valore: (g) => g.serieConferme,
},
]; ];
export type SerieStato = { export type SerieStato = {
+13 -4
View File
@@ -90,6 +90,13 @@ function Calendario() {
// Prossimi 3 eventi da oggi in avanti (indipendenti dal mese selezionato nella griglia). // Prossimi 3 eventi da oggi in avanti (indipendenti dal mese selezionato nella griglia).
const oggiIso = oggi ? `${oggi.anno}-${pad2(oggi.mese + 1)}-${pad2(oggi.giorno)}` : null; const oggiIso = oggi ? `${oggi.anno}-${pad2(oggi.mese + 1)}-${pad2(oggi.giorno)}` : null;
const prossimiEventi = oggiIso ? eventi.filter((e) => e.data >= oggiIso).slice(0, 3) : []; const prossimiEventi = oggiIso ? eventi.filter((e) => e.data >= oggiIso).slice(0, 3) : [];
// Solo i compleanni del mese in corso non ancora passati (oggi compreso), a prescindere dal mese
// mostrato nella griglia.
const compleanniMeseCorrente =
oggi && oggiIso
? compleanniEventi(rosa, oggi.anno).filter((c) => c.data.startsWith(oggiIso.slice(0, 7)))
: [];
const prossimiCompleanni = oggiIso ? compleanniMeseCorrente.filter((c) => c.data >= oggiIso) : [];
return ( return (
<> <>
@@ -270,13 +277,15 @@ function Calendario() {
</div> </div>
</Section> </Section>
<Section titolo="Compleanni"> <Section titolo="Prossimi compleanni">
<div className="space-y-3"> <div className="space-y-3">
{compleanniMese.length > 0 ? ( {prossimiCompleanni.length > 0 ? (
compleanniMese.map((c) => <EventoCard key={c.id} evento={c} />) prossimiCompleanni.map((c) => <EventoCard key={c.id} evento={c} />)
) : ( ) : (
<p className="rounded-3xl bg-card p-4 text-center text-[15px] text-muted-foreground shadow-card"> <p className="rounded-3xl bg-card p-4 text-center text-[15px] text-muted-foreground shadow-card">
Nessun compleanno in {mesiIT[mese]!.toLowerCase()} {compleanniMeseCorrente.length > 0
? "Nessun altro compleanno questo mese"
: "Nessun compleanno questo mese"}
</p> </p>
)} )}
</div> </div>
@@ -1,204 +0,0 @@
/**
* Badge segreto Mai un forfait end-to-end contro il database locale:
* `bun test/integration/s-mai-forfait-badge.test.ts`.
*
* I test unitari (`test/unit/presenze.test.ts`, `test/unit/badges.test.ts`) verificano
* `serieConferme()`, `contaPresenzeGiocatore()` e `statoBadge()` come funzioni pure. Qui invece
* si scrivono eventi con `creato_il` esplicito e risposte con `risposto_il` esplicito su
* `eventi_app`/`risposte_presenze`, si rileggono via REST e si passa il risultato attraverso
* entrambe le funzioni fino a `statoBadge()` sul badge segreto `s-mai-forfait`, che è l'unico a
* combinare due statistiche indipendenti (serie di conferme rapide **e** presenze totali).
*
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
* usa id con il prefisso `test-s-mai-forfait-badge`, che nessun dato vero può avere.
*/
import assert from "node:assert/strict";
import { badgeSegreti, statoBadge } from "@/lib/badges";
import { serieConferme, contaPresenzeGiocatore } from "@/lib/presenze";
import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi";
import { giocatori, type Giocatore } from "@/lib/crapp-data";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
import type { Stato } from "@/lib/crapp-data";
import type { MappaPresenze } from "@/lib/presenze";
const locale = statoLocale();
if (!locale) {
salta("badge Mai un forfait sul database", "stack locale non attivo (npx supabase start)");
riepilogo("s-mai-forfait-badge");
} else {
const { url: URL_BASE, servizio: SERVIZIO } = locale;
console.log(`badge Mai un forfait su ${URL_BASE}`);
const PREFISSO = "test-s-mai-forfait-badge";
const def = badgeSegreti.find((b) => b.id === "s-mai-forfait")!;
const OGGI = "2099-01-01";
const rest = (percorso: string, init?: RequestInit) =>
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
...init,
headers: {
apikey: SERVIZIO,
Authorization: `Bearer ${SERVIZIO}`,
"content-type": "application/json",
...(init?.headers ?? {}),
},
});
const dataEvento = (i: number) => {
const d = new Date(Date.UTC(2020, 0, 1));
d.setUTCDate(d.getUTCDate() + i - 1);
return d.toISOString().slice(0, 10);
};
async function creaEvento(id: string, i: number) {
const res = await rest("eventi_app", {
method: "POST",
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
body: JSON.stringify({
id,
tipo: "allenamento",
titolo: `Evento ${id}`,
data: dataEvento(i),
creato_il: `${dataEvento(i)}T08:00:00Z`,
}),
});
if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`);
}
async function rispondi(eventoId: string, giocatoreId: string, i: number) {
const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", {
method: "POST",
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
body: JSON.stringify({
evento_id: eventoId,
giocatore_id: giocatoreId,
stato: "presente",
risposto_il: `${dataEvento(i)}T08:30:00Z`,
}),
});
if (!res.ok) throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`);
}
async function leggiEventi(): Promise<Evento[]> {
const res = await rest(
"eventi_app?id=like." +
PREFISSO +
"*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il",
);
const righe = (await res.json()) as RigaEvento[];
return righe.map(daRiga);
}
async function leggiPresenzeETempi(): Promise<{
presenze: MappaPresenze;
tempi: Record<string, Record<string, string>>;
}> {
const res = await rest(
`risposte_presenze?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,stato,risposto_il`,
);
const righe = (await res.json()) as Array<{
evento_id: string;
giocatore_id: string;
stato: Stato;
risposto_il: string;
}>;
const presenze: MappaPresenze = {};
const tempi: Record<string, Record<string, string>> = {};
for (const r of righe) {
(presenze[r.evento_id] ??= {})[r.giocatore_id] = r.stato;
(tempi[r.evento_id] ??= {})[r.giocatore_id] = r.risposto_il;
}
return { presenze, tempi };
}
function giocatoreAzzerato(serieConferme: number, presenze: number): Giocatore {
return {
...giocatori[0]!,
serieConferme,
presenze,
mvp: 0,
mediaVoto: 0,
votiPagella: 0,
palloni: 0,
};
}
try {
await prova(
"il segreto si sblocca solo quando entrambe le condizioni sono vere insieme",
async () => {
// "mf1": 15 eventi passati, presente e confermato in fretta a tutti — soddisfa
// ampiamente sia la serie di conferme (>=10) sia le presenze (>=15).
for (let i = 1; i <= 15; i += 1) {
const id = `${PREFISSO}-a${String(i).padStart(2, "0")}`;
await creaEvento(id, i);
await rispondi(id, "mf1", i);
}
const eventi15 = await leggiEventi();
const { presenze: p15, tempi: t15 } = await leggiPresenzeETempi();
// Solo 9 eventi: serie di conferme appena sotto soglia, presenze sotto soglia.
const eventi9 = eventi15.slice(0, 9);
const serie9 = serieConferme("mf1", eventi9, t15, OGGI);
const pres9 = contaPresenzeGiocatore("mf1", eventi9, p15, OGGI);
assert.equal(serie9, 9);
assert.equal(pres9, 9);
assert.equal(
statoBadge(def, giocatoreAzzerato(serie9, pres9)).grado,
null,
"9 conferme e 9 presenze: nessuna delle due soglie raggiunta",
);
const serie15 = serieConferme("mf1", eventi15, t15, OGGI);
const pres15 = contaPresenzeGiocatore("mf1", eventi15, p15, OGGI);
assert.equal(serie15, 15);
assert.equal(pres15, 15);
assert.equal(
statoBadge(def, giocatoreAzzerato(serie15, pres15)).grado,
"oro",
"serie conferme >=10 e presenze >=15: il segreto si sblocca",
);
},
);
await prova("una risposta lenta rompe la serie ma non le presenze già accumulate", async () => {
// Un sedicesimo evento con risposta arrivata oltre le 24h: la serie conferme torna a 0,
// ma le presenze (indipendenti) restano a 16 — il segreto deve richiudersi, non serve
// ripartire da zero anche sulle presenze.
const idLento = `${PREFISSO}-a16`;
await creaEvento(idLento, 16);
const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", {
method: "POST",
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
body: JSON.stringify({
evento_id: idLento,
giocatore_id: "mf1",
stato: "presente",
risposto_il: `${dataEvento(18)}T08:00:00Z`, // 2 giorni dopo la convocazione
}),
});
if (!res.ok)
throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`);
const eventi = await leggiEventi();
const { presenze, tempi } = await leggiPresenzeETempi();
const serie = serieConferme("mf1", eventi, tempi, OGGI);
const conto = contaPresenzeGiocatore("mf1", eventi, presenze, OGGI);
assert.equal(serie, 0, "la risposta lenta azzera la serie di conferme");
assert.equal(conto, 16, "le presenze restano quelle di sempre, indipendenti dalla serie");
assert.equal(
statoBadge(def, giocatoreAzzerato(serie, conto)).grado,
null,
"presenze abbondanti ma serie azzerata: il segreto si richiude",
);
});
} finally {
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
}
riepilogo("s-mai-forfait-badge");
}
+2 -2
View File
@@ -254,8 +254,8 @@ if (!locale) {
// --- presenze: la risposta si cambia, il cronometro no -------------------------- // --- presenze: la risposta si cambia, il cronometro no --------------------------
// Due colonne che sembrano la stessa cosa e non lo sono: `risposto_il` è la PRIMA // Due colonne che sembrano la stessa cosa e non lo sono: `risposto_il` è la PRIMA
// risposta e alimenta la serie "Conferme 24h", `aggiornato_il` è l'ultima modifica e non // risposta, `aggiornato_il` è l'ultima modifica; oggi nessuna statistica le usa (la serie
// alimenta niente. Il trigger `risposte_presenze_risposto_il_immutabile` (M9) tiene ferma // «Conferme 24h» è stata rimossa). Il trigger `risposte_presenze_risposto_il_immutabile` (M9) tiene ferma
// la prima: senza, chi risponde subito e ci ripensa una settimana dopo risulterebbe lento. // la prima: senza, chi risponde subito e ci ripensa una settimana dopo risulterebbe lento.
await prova( await prova(
"la risposta di presenza si aggiorna senza far ripartire il cronometro", "la risposta di presenza si aggiorna senza far ripartire il cronometro",
@@ -1,229 +0,0 @@
/**
* Badge Risposta lampo end-to-end contro il database locale:
* `bun test/integration/serie-conferme-badge.test.ts`.
*
* I test unitari (`test/unit/presenze.test.ts`, `test/unit/badges.test.ts`) verificano
* `serieConferme()` e `statoBadge()` come funzioni pure, con eventi e risposte costruiti a
* mano. Qui invece si scrivono allenamenti/partite ed eventi veri su `eventi_app`, e risposte
* vere su `risposte_presenze` con `creato_il`/`risposto_il` espliciti, si rileggono via REST
* con la stessa forma di `daRiga()`/`fetchPresenze()`, e si passa il risultato attraverso
* `serieConferme()` fino a `statoBadge()` sul badge `serie-conferme`: se una colonna cambia
* nome, o la mappatura `creato_il`/`risposto_il` -> `creatoIl`/`tempi` si rompe, qui il grado
* del badge torna sbagliato anche se i test unitari restano verdi, perché quelli non toccano
* mai il database.
*
* Copre due comportamenti non ovvi con dati veri, non solo con numeri scelti a mano:
* - una risposta arrivata oltre le 24h dalla convocazione azzera la serie, anche dopo una
* striscia lunga;
* - partite e allenamenti contano nella stessa serie, senza bisogno di un filtro per tipo.
*
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
* usa id con il prefisso `test-serie-conferme-badge`, che nessun dato vero può avere.
*/
import assert from "node:assert/strict";
import { badgeDefs, statoBadge } from "@/lib/badges";
import { serieConferme } from "@/lib/presenze";
import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi";
import { giocatori, type Giocatore } from "@/lib/crapp-data";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
const locale = statoLocale();
if (!locale) {
salta("badge Risposta lampo sul database", "stack locale non attivo (npx supabase start)");
riepilogo("serie-conferme-badge");
} else {
const { url: URL_BASE, servizio: SERVIZIO } = locale;
console.log(`badge Risposta lampo su ${URL_BASE}`);
const PREFISSO = "test-serie-conferme-badge";
const serieDef = badgeDefs.find((b) => b.id === "serie-conferme")!;
// "Oggi" fissato nel futuro: gli eventi scritti sotto sono datati nel passato remoto, così
// restano "passati" (quindi contati) a prescindere da quando gira il test.
const OGGI = "2099-01-01";
const rest = (percorso: string, init?: RequestInit) =>
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
...init,
headers: {
apikey: SERVIZIO,
Authorization: `Bearer ${SERVIZIO}`,
"content-type": "application/json",
...(init?.headers ?? {}),
},
});
/** `2020-01-01` per i=1, avanzando di un giorno per ogni evento: niente collisioni di data. */
const dataEvento = (i: number) => {
const d = new Date(Date.UTC(2020, 0, 1));
d.setUTCDate(d.getUTCDate() + i - 1);
return d.toISOString().slice(0, 10);
};
/** Istante di convocazione: le 8 del mattino del giorno dell'evento. */
const creazioneEvento = (i: number) => `${dataEvento(i)}T08:00:00Z`;
async function creaEvento(
id: string,
i: number,
tipo: "allenamento" | "partita" = "allenamento",
) {
const res = await rest("eventi_app", {
method: "POST",
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
body: JSON.stringify({
id,
tipo,
titolo: `Evento ${id}`,
data: dataEvento(i),
creato_il: creazioneEvento(i),
}),
});
if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`);
}
async function rispondi(
eventoId: string,
giocatoreId: string,
stato: string,
rispostoIl: string,
) {
const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", {
method: "POST",
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
body: JSON.stringify({
evento_id: eventoId,
giocatore_id: giocatoreId,
stato,
risposto_il: rispostoIl,
}),
});
if (!res.ok) throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`);
}
async function leggiEventi(): Promise<Evento[]> {
const res = await rest(
"eventi_app?id=like." +
PREFISSO +
"*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il",
);
const righe = (await res.json()) as RigaEvento[];
return righe.map(daRiga);
}
/** Stessa forma di `fetchPresenze()`: eventoId -> giocatoreId -> istante della risposta. */
async function leggiTempi(): Promise<Record<string, Record<string, string>>> {
const res = await rest(
`risposte_presenze?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,risposto_il`,
);
const righe = (await res.json()) as Array<{
evento_id: string;
giocatore_id: string;
risposto_il: string;
}>;
const mappa: Record<string, Record<string, string>> = {};
for (const r of righe) (mappa[r.evento_id] ??= {})[r.giocatore_id] = r.risposto_il;
return mappa;
}
function giocatoreAzzerato(serieConferme: number): Giocatore {
return {
...giocatori[0]!,
serieConferme,
mvp: 0,
mediaVoto: 0,
votiPagella: 0,
palloni: 0,
presenze: 0,
};
}
try {
await prova(
"il badge passa da bronzo a oro con conferme rapide vere, e una risposta lenta lo azzera",
async () => {
// 15 eventi passati, tutti confermati mezz'ora dopo la convocazione per "sc1":
// bronzo (3), poi argento (8), poi oro (15) man mano che si rileggono i dati.
for (let i = 1; i <= 15; i += 1) {
const id = `${PREFISSO}-a${String(i).padStart(2, "0")}`;
await creaEvento(id, i);
await rispondi(id, "sc1", "presente", `${dataEvento(i)}T08:30:00Z`);
}
const eventi15 = await leggiEventi();
const tempi15 = await leggiTempi();
const serieA3 = serieConferme("sc1", eventi15.slice(0, 3), tempi15, OGGI);
assert.equal(serieA3, 3);
assert.equal(
statoBadge(serieDef, giocatoreAzzerato(serieA3)).grado,
"bronzo",
"3 conferme rapide di fila: soglia bronzo",
);
const serieA8 = serieConferme("sc1", eventi15.slice(0, 8), tempi15, OGGI);
assert.equal(serieA8, 8);
assert.equal(
statoBadge(serieDef, giocatoreAzzerato(serieA8)).grado,
"argento",
"8 conferme rapide di fila: soglia argento",
);
const serieCompleta = serieConferme("sc1", eventi15, tempi15, OGGI);
assert.equal(serieCompleta, 15, "confermato in fretta a tutti e 15, nessun buco");
assert.equal(
statoBadge(serieDef, giocatoreAzzerato(serieCompleta)).grado,
"oro",
"15 conferme rapide di fila: soglia oro",
);
// Un sedicesimo evento con una risposta arrivata dopo 2 giorni azzera la serie.
const idLento = `${PREFISSO}-a16`;
await creaEvento(idLento, 16);
await rispondi(idLento, "sc1", "presente", `${dataEvento(18)}T08:00:00Z`);
const eventiConRitardo = await leggiEventi();
const tempiConRitardo = await leggiTempi();
const serieDopoRitardo = serieConferme("sc1", eventiConRitardo, tempiConRitardo, OGGI);
assert.equal(
serieDopoRitardo,
0,
"una risposta oltre le 24h azzera la serie, anche dopo 15 conferme di fila",
);
assert.equal(
statoBadge(serieDef, giocatoreAzzerato(serieDopoRitardo)).grado,
null,
"serie azzerata: nessun grado, si riparte da zero",
);
},
);
await prova(
"partite e allenamenti contano nella stessa serie, senza filtro per tipo",
async () => {
// "sc2": conferma rapida su un allenamento, poi su una partita, poi su un altro
// allenamento: la serie sale a 3 senza bisogno che siano tutti dello stesso tipo.
await creaEvento(`${PREFISSO}-b01`, 30, "allenamento");
await creaEvento(`${PREFISSO}-b02`, 31, "partita");
await creaEvento(`${PREFISSO}-b03`, 32, "allenamento");
await rispondi(`${PREFISSO}-b01`, "sc2", "presente", `${dataEvento(30)}T08:30:00Z`);
await rispondi(`${PREFISSO}-b02`, "sc2", "presente", `${dataEvento(31)}T08:30:00Z`);
await rispondi(`${PREFISSO}-b03`, "sc2", "presente", `${dataEvento(32)}T08:30:00Z`);
const eventiLetti = await leggiEventi();
const tempiLetti = await leggiTempi();
const serie = serieConferme("sc2", eventiLetti, tempiLetti, OGGI);
assert.equal(serie, 3, "partita in mezzo a due allenamenti: la serie resta a 3");
assert.equal(
statoBadge(serieDef, giocatoreAzzerato(serie)).grado,
"bronzo",
"3 conferme rapide di fila, tipo misto: soglia bronzo",
);
},
);
} finally {
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
}
riepilogo("serie-conferme-badge");
}
-42
View File
@@ -34,7 +34,6 @@ function g(valori: Partial<Giocatore> = {}): Giocatore {
streak: 0, streak: 0,
serieAllenamenti: 0, serieAllenamenti: 0,
seriePartite: 0, seriePartite: 0,
serieConferme: 0,
...valori, ...valori,
}; };
} }
@@ -146,16 +145,6 @@ assert.equal(gradoRaggiunto(serieAllenamentiDef, 9), "argento");
assert.equal(gradoRaggiunto(serieAllenamentiDef, 10), "oro"); assert.equal(gradoRaggiunto(serieAllenamentiDef, 10), "oro");
assert.equal(gradoRaggiunto(serieAllenamentiDef, 50), "oro", "oltre l'oro resta oro"); assert.equal(gradoRaggiunto(serieAllenamentiDef, 50), "oro", "oltre l'oro resta oro");
// --- serie-conferme: soglie 3 / 8 / 15 ----------------------------------------
const serieConfermeDef = badgeDefs.find((b) => b.id === "serie-conferme")!;
assert.equal(gradoRaggiunto(serieConfermeDef, 2), null, "sotto la prima soglia nessun grado");
assert.equal(gradoRaggiunto(serieConfermeDef, 3), "bronzo", "la soglia è inclusiva");
assert.equal(gradoRaggiunto(serieConfermeDef, 7), "bronzo");
assert.equal(gradoRaggiunto(serieConfermeDef, 8), "argento");
assert.equal(gradoRaggiunto(serieConfermeDef, 14), "argento");
assert.equal(gradoRaggiunto(serieConfermeDef, 15), "oro");
assert.equal(gradoRaggiunto(serieConfermeDef, 50), "oro", "oltre l'oro resta oro");
// --- badgeGiocatore ---------------------------------------------------------- // --- badgeGiocatore ----------------------------------------------------------
assert.equal(badgeGiocatore(g()).length, badgeDefs.length, "i badge normali sono sempre tutti"); assert.equal(badgeGiocatore(g()).length, badgeDefs.length, "i badge normali sono sempre tutti");
assert.ok( assert.ok(
@@ -200,36 +189,6 @@ assert.deepEqual(
["s-cacche"], ["s-cacche"],
); );
assert.equal(badgeSegretiSbloccati(g({ cacche: 2 })).length, 0, "2 cacche non bastano"); assert.equal(badgeSegretiSbloccati(g({ cacche: 2 })).length, 0, "2 cacche non bastano");
assert.deepEqual(
badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 15 })).map((b) => b.def.id),
["s-mai-forfait"],
);
assert.equal(
badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 14 })).length,
0,
"servono entrambe le condizioni",
);
// Confini isolati: ogni soglia testata da sola, con l'altra abbondantemente sopra.
assert.equal(
badgeSegretiSbloccati(g({ serieConferme: 9, presenze: 30 })).length,
0,
"serieConferme appena sotto soglia, presenze abbondanti: non basta",
);
assert.deepEqual(
badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 30 })).map((b) => b.def.id),
["s-mai-forfait"],
"serieConferme esattamente al confine, presenze abbondanti: sblocca",
);
assert.equal(
badgeSegretiSbloccati(g({ serieConferme: 30, presenze: 14 })).length,
0,
"presenze appena sotto soglia, serieConferme abbondante: non basta",
);
assert.deepEqual(
badgeSegretiSbloccati(g({ serieConferme: 30, presenze: 15 })).map((b) => b.def.id),
["s-mai-forfait"],
"presenze esattamente al confine, serieConferme abbondante: sblocca",
);
// --- collezioneBadge --------------------------------------------------------- // --- collezioneBadge ---------------------------------------------------------
const vuota = collezioneBadge(g()); const vuota = collezioneBadge(g());
@@ -262,7 +221,6 @@ assert.equal(
palloni: 10, palloni: 10,
presenze: 30, presenze: 30,
serieAllenamenti: 10, serieAllenamenti: 10,
serieConferme: 15,
}), }),
), ),
null, null,
+1 -2
View File
@@ -19,7 +19,6 @@ function g(valori: Partial<Giocatore> = {}): Giocatore {
streak: 0, streak: 0,
serieAllenamenti: 0, serieAllenamenti: 0,
seriePartite: 0, seriePartite: 0,
serieConferme: 0,
...valori, ...valori,
}; };
} }
@@ -57,7 +56,7 @@ assert.ok(
// --- serie: solo sul traguardo esatto ---------------------------------------- // --- serie: solo sul traguardo esatto ----------------------------------------
assert.ok(ids(g({ serieAllenamenti: 3 })).includes("serie:allenamenti:3")); assert.ok(ids(g({ serieAllenamenti: 3 })).includes("serie:allenamenti:3"));
assert.ok(!ids(g({ serieAllenamenti: 4 })).includes("serie:allenamenti:4"), "4 non è un traguardo"); assert.ok(!ids(g({ serieAllenamenti: 4 })).includes("serie:allenamenti:4"), "4 non è un traguardo");
assert.ok(ids(g({ serieConferme: 8 })).includes("serie:conferme:8")); assert.ok(ids(g({ seriePartite: 5 })).includes("serie:partite:5"));
// --- obiettivi di squadra: uno solo, tra il 90% e il 99% -------------------- // --- obiettivi di squadra: uno solo, tra il 90% e il 99% --------------------
const obiettivo = (id: string, valore: number): ObiettivoSquadra => ({ const obiettivo = (id: string, valore: number): ObiettivoSquadra => ({
+1 -84
View File
@@ -7,7 +7,6 @@ import {
contaPresenzeGiocatore, contaPresenzeGiocatore,
destinatariSollecito, destinatariSollecito,
includeInfortunato, includeInfortunato,
serieConferme,
serieConsecutiva, serieConsecutiva,
testoSollecito, testoSollecito,
totaliEventiGiocatore, totaliEventiGiocatore,
@@ -218,91 +217,9 @@ assert.ok(
"senza luogo la riga manca", "senza luogo la riga manca",
); );
// --- serie conferme: risposta entro 24h dalla convocazione --------------------
const convocati = (id: string, data: string, creatoIl?: string): Evento => ({
...ev(id, "allenamento", data),
...(creatoIl === undefined ? {} : { creatoIl }),
});
const conConvocazione: Evento[] = [
convocati("c1", "2026-08-06", "2026-08-01T10:00:00Z"),
convocati("c2", "2026-08-13", "2026-08-08T10:00:00Z"),
convocati("c3", "2026-08-20", "2026-08-15T10:00:00Z"),
convocati("c4", "2026-08-27"), // evento generato dal client: nessuna convocazione tracciata
convocati("c5", "2026-09-10", "2026-09-05T10:00:00Z"), // futuro
];
const tempi: MappaTempiRisposta = {
c1: { g1: "2026-08-01T11:00:00Z" }, // un'ora dopo
c2: { g1: "2026-08-10T10:00:00Z" }, // due giorni dopo: buco
c3: { g1: "2026-08-16T09:59:00Z" }, // appena dentro le 24h
c5: { g1: "2026-09-05T10:30:00Z" },
};
assert.equal(serieConferme("g1", conConvocazione, tempi, OGGI), 1, "il ritardo su c2 azzera");
assert.equal(
serieConferme("g1", conConvocazione.slice(0, 1), tempi, OGGI),
1,
"una risposta rapida vale 1",
);
assert.equal(
serieConferme("g1", [conConvocazione[0]!, conConvocazione[3]!, conConvocazione[2]!], tempi, OGGI),
2,
"un evento senza istante di convocazione viene saltato, non spezza la serie",
);
assert.equal(serieConferme("g2", conConvocazione, tempi, OGGI), 0, "chi non risponde è a zero");
// Chi non è convocato non spezza la serie conferme di nessun altro (stesso filtro condiviso
// con `serieConsecutiva`, vedi sopra).
const c6Ristretto: Evento = {
...convocati("c6", "2026-08-08", "2026-08-03T10:00:00Z"),
convocati: ["g9"],
};
assert.equal(
serieConferme("g1", [conConvocazione[0]!, c6Ristretto], tempi, OGGI),
1,
"un evento convocato solo per un altro giocatore resta fuori, la serie di g1 non ne risente",
);
// Le conferme contano partite e allenamenti insieme: nessuna versione per tipo.
const mistiTipo: Evento[] = [
convocati("m1", "2026-08-06", "2026-08-01T10:00:00Z"),
{ ...convocati("m2", "2026-08-13", "2026-08-08T10:00:00Z"), tipo: "partita" },
];
const tempiMisti: MappaTempiRisposta = {
m1: { g1: "2026-08-01T11:00:00Z" }, // un'ora dopo
m2: { g1: "2026-08-08T11:00:00Z" }, // un'ora dopo, ma è una partita
};
assert.equal(
serieConferme("g1", mistiTipo, tempiMisti, OGGI),
2,
"partite e allenamenti si sommano nella stessa serie",
);
// Il confronto con le 24h è inclusivo: esattamente al bordo conta, un secondo oltre azzera.
const bordo: Evento[] = [convocati("b1", "2026-08-06", "2026-08-01T10:00:00Z")];
assert.equal(
serieConferme("g1", bordo, { b1: { g1: "2026-08-02T10:00:00Z" } }, OGGI),
1,
"esattamente 24h dopo: il confronto è <=, quindi conta",
);
assert.equal(
serieConferme("g1", bordo, { b1: { g1: "2026-08-02T10:00:01Z" } }, OGGI),
0,
"un secondo oltre le 24h azzera",
);
// Un evento futuro non conta ancora, anche con una risposta rapidissima già registrata.
const futuro: Evento[] = [convocati("f1", "2026-09-10", "2026-09-05T10:00:00Z")];
assert.equal(
serieConferme("g1", futuro, { f1: { g1: "2026-09-05T10:05:00Z" } }, OGGI),
0,
"l'evento di domani non è ancora passato, non entra nel calcolo",
);
// --- cache locale dopo una risposta: il cronometro non riparte ---------------- // --- cache locale dopo una risposta: il cronometro non riparte ----------------
// Stessa regola del database: `risposto_il` è la PRIMA risposta e un trigger la congela. // Stessa regola del database: `risposto_il` è la PRIMA risposta e un trigger la congela.
// Qui la cache deve imitarla, altrimenti la serie "Conferme 24h" mente fino al refresh. // Qui la cache deve imitarla, altrimenti la cache mente fino al refresh.
const PRIMA = "2026-08-01T10:00:00Z"; const PRIMA = "2026-08-01T10:00:00Z";
const POI = "2026-08-08T10:00:00Z"; const POI = "2026-08-08T10:00:00Z";
+6 -6
View File
@@ -4,7 +4,7 @@ import { giocatori, type Giocatore } from "@/lib/crapp-data";
import { aggiornaSerie, serieDefs, serieGiocatore, serieMigliore, statoSerie } from "@/lib/serie"; import { aggiornaSerie, serieDefs, serieGiocatore, serieMigliore, statoSerie } from "@/lib/serie";
function g(valori: Partial<Giocatore> = {}): Giocatore { function g(valori: Partial<Giocatore> = {}): Giocatore {
return { ...giocatori[0]!, serieAllenamenti: 0, seriePartite: 0, serieConferme: 0, ...valori }; return { ...giocatori[0]!, serieAllenamenti: 0, seriePartite: 0, ...valori };
} }
const allenamenti = serieDefs.find((d) => d.tipo === "allenamenti")!; const allenamenti = serieDefs.find((d) => d.tipo === "allenamenti")!;
@@ -45,16 +45,16 @@ assert.deepEqual([fuoriScala.prossimo, fuoriScala.manca, fuoriScala.progresso],
assert.equal(fuoriScala.messaggio, "Serie leggendaria: sei fuori scala!"); assert.equal(fuoriScala.messaggio, "Serie leggendaria: sei fuori scala!");
// --- serieGiocatore / serieMigliore ------------------------------------------ // --- serieGiocatore / serieMigliore ------------------------------------------
const tutte = serieGiocatore(g({ serieAllenamenti: 4, seriePartite: 1, serieConferme: 9 })); const tutte = serieGiocatore(g({ serieAllenamenti: 4, seriePartite: 6 }));
assert.equal(tutte.length, serieDefs.length, "tre serie indipendenti"); assert.equal(tutte.length, serieDefs.length, "due serie indipendenti");
assert.deepEqual( assert.deepEqual(
tutte.map((s) => s.valore), tutte.map((s) => s.valore),
[4, 1, 9], [4, 6],
"ogni serie legge il proprio contatore", "ogni serie legge il proprio contatore",
); );
const migliore = serieMigliore(g({ serieAllenamenti: 4, seriePartite: 1, serieConferme: 9 })); const migliore = serieMigliore(g({ serieAllenamenti: 4, seriePartite: 6 }));
assert.equal(migliore.def.tipo, "conferme", "in home si mostra la serie più lunga"); assert.equal(migliore.def.tipo, "partite", "in home si mostra la serie più lunga");
assert.ok(serieMigliore(g()), "anche a zero c'è sempre una serie da mostrare"); assert.ok(serieMigliore(g()), "anche a zero c'è sempre una serie da mostrare");
// --- invarianti sulle definizioni -------------------------------------------- // --- invarianti sulle definizioni --------------------------------------------