Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa0dd9c60 | ||
|
|
bfb768fb5a |
No files matched your search
+4
-3
@@ -9,9 +9,10 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
|
||||
|
||||
## Stato generale
|
||||
|
||||
- **Versione:** 1.3.0 (tag `v1.3.0` su `main`) — notifica di compleanno, obiettivi cliccabili con card di
|
||||
dettaglio, presenze calcolate sui convocati, avvisi in Home ridisegnati, caratteri più grandi, tessera CSI
|
||||
tolta dalla dashboard. Dettaglio in [docs/CHANGELOG.md](docs/CHANGELOG.md).
|
||||
- **Versione:** 1.3.1 (in produzione su `main` la 1.3.0, tag `v1.3.0`) — il Calendario mostra solo i prossimi
|
||||
compleanni del mese; tolta la serie «Conferme 24h» con i badge «Risposta lampo» e «Mai un forfait». La 1.3.0
|
||||
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
|
||||
Project Ref `hetycilxgkdmccelwerq` non si usano più.
|
||||
- **Migration:** 42 file in `supabase/migrations/`, fino a `m31_notifica_compleanno`. Le prime 41 (fino a
|
||||
|
||||
@@ -13,6 +13,23 @@ versioni `0.y.z` erano pre-release.
|
||||
|
||||
## [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
|
||||
|
||||
Gli auguri di compleanno arrivano da soli, gli obiettivi di squadra si possono toccare per capire come funzionano
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ Colonne di `giocatori_squadra` che meritano una nota:
|
||||
| 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. |
|
||||
| `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. |
|
||||
| `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). |
|
||||
|
||||
+1
-1
@@ -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. |
|
||||
| **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). |
|
||||
| **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). |
|
||||
| **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). |
|
||||
|
||||
+28
-34
@@ -76,7 +76,7 @@ badge assegnati per voto dai compagni.
|
||||
|
||||
## 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
|
||||
condizioni esatte, è una scelta deliberata per la documentazione tecnica, non una fuga di
|
||||
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 |
|
||||
| `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-conferme` | Risposta lampo | conferme di presenza consecutive date entro 24h dalla convocazione (`g.serieConferme`) | 3 / 8 / 15 |
|
||||
|
||||
### 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 è
|
||||
`grado !== null`, non il suo valore, ed è così che li legge `badgeSegretiSbloccati()`.
|
||||
|
||||
| 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-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-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`) |
|
||||
| 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-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-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)
|
||||
|
||||
@@ -149,24 +147,22 @@ dicono i file, non questa pagina: leggili prima di cambiare una soglia.
|
||||
|
||||
### Riepilogo per badge
|
||||
|
||||
| # | id | tipo | test unit | test integration |
|
||||
| --- | ------------------- | ------- | ----------------------------- | --------------------------------------------- |
|
||||
| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) |
|
||||
| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) |
|
||||
| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) |
|
||||
| 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) |
|
||||
| 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) |
|
||||
| 6 | `serie-conferme` | normale | ✅ (vedi Limiti noti) | ✅ (`serie-conferme-badge`) |
|
||||
| 7 | `s-tiebreak` | segreto | ✅ (soglia minima voti) | ✅ (`s-tiebreak-badge`) |
|
||||
| 8 | `s-mai-forfait` | segreto | ✅ | ✅ (`s-mai-forfait-badge`) |
|
||||
| 9 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) |
|
||||
| 10 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) |
|
||||
| 11 | `s-cacche` | segreto | ✅ | ✅ (`s-cacche-badge`) |
|
||||
| 12 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) |
|
||||
| 13 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) |
|
||||
| 14 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) |
|
||||
| 15 | `meme` | social | ✅ | ✅ (`badge-social`) |
|
||||
| 16 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) |
|
||||
| # | id | tipo | test unit | test integration |
|
||||
| --- | ------------------- | ------------ | ----------------------------- | --------------------------------------------- |
|
||||
| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) |
|
||||
| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) |
|
||||
| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) |
|
||||
| 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) |
|
||||
| 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) |
|
||||
| 6 | 7 | `s-tiebreak` | segreto | ✅ (soglia minima voti) | ✅ (`s-tiebreak-badge`) |
|
||||
| 6 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) |
|
||||
| 7 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) |
|
||||
| 8 | `s-cacche` | segreto | ✅ | ✅ (`s-cacche-badge`) |
|
||||
| 9 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) |
|
||||
| 10 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) |
|
||||
| 11 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) |
|
||||
| 12 | `meme` | social | ✅ | ✅ (`badge-social`) |
|
||||
| 13 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -193,11 +189,9 @@ dicono i file, non questa pagina: leggili prima di cambiare una soglia.
|
||||
|
||||
## Limiti noti
|
||||
|
||||
- **Dipendenza dal modulo [Serie](serie-presenze.md)**: i badge "Sempre in palestra",
|
||||
"Risposta lampo" e il segreto "Mai un forfait" si muovono solo se cambiano le serie. Le
|
||||
serie sono calcolate sui dati reali dalla migration `m9` in avanti, ma "Risposta lampo" e
|
||||
"Mai un forfait" dipendono da `serieConferme`, e `risposto_il` non è ricostruibile per le
|
||||
risposte precedenti a `m9`: su quelle righe la serie è un'approssimazione.
|
||||
- **Dipendenza dal modulo [Serie](serie-presenze.md)**: il badge «Sempre in palestra» si muove solo se cambia la serie
|
||||
Allenamenti. Il badge «Risposta lampo» e il segreto «Mai un forfait», basati sulla serie «Conferme 24h», sono stati
|
||||
rimossi il 2026-10-06.
|
||||
- Nessuno storico dei badge sbloccati: se cambiano le soglie o i dati sorgente, un badge già
|
||||
"ottenuto" può sparire o apparire retroattivamente.
|
||||
- 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
|
||||
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
|
||||
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
|
||||
@@ -238,7 +232,7 @@ ancora correggere un voto anche fuori convocazione o dopo la chiusura.
|
||||
## Evoluzioni possibili
|
||||
|
||||
- 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.
|
||||
- Rimuovere `badgeSbloccati()` (codice morto) o documentarne lo scopo.
|
||||
- Aggiungere un vincolo (CHECK o FK) sulla colonna `categoria` di `badge_social_voti`.
|
||||
|
||||
@@ -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:
|
||||
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
|
||||
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
|
||||
(`usePuoGestireEventi()`, [DD-034](../decisions/DD-034.md)):
|
||||
crea, modifica ed elimina un evento, sceglie i convocati tra i soli giocatori
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 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`,
|
||||
`src/components/crapp/SerieCard.tsx`
|
||||
**Migration collegata:** `m9_risposte_presenze_risposto_il`
|
||||
**Test:** `test/unit/serie.test.ts`, `test/unit/presenze.test.ts`,
|
||||
`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 |
|
||||
| ------------- | ------------------ | ------------------------------------------------------------ | ------------ |
|
||||
| `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 |
|
||||
| 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 |
|
||||
|
||||
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 consecutive" in `src/routes/index.tsx`, `src/routes/squadra.tsx` e
|
||||
`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
|
||||
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 |
|
||||
| Colonna | Cosa registra | Chi la usa |
|
||||
| --------------- | --------------------- | -------------------------------------- |
|
||||
| `risposto_il` | la **prima** risposta | nessuna statistica (ex «Conferme 24h») |
|
||||
| `aggiornato_il` | l'**ultima** modifica | nessuna statistica |
|
||||
|
||||
Cancellare la risposta (`stato: null` → DELETE) elimina anche `risposto_il`: se il giocatore
|
||||
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.
|
||||
- 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:
|
||||
@@ -263,8 +246,6 @@ 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 |
|
||||
|
||||
---
|
||||
@@ -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
|
||||
`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
|
||||
offset scritto a mano. È lo stesso `oggi` di `serieConsecutiva()` e del
|
||||
conteggio presenze — prima `serieConsecutiva()` e `serieConferme()` (poi rimossa) 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".
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "crapp",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "crapp",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.111.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "crapp",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
|
||||
@@ -3,9 +3,7 @@ import {
|
||||
Repeat,
|
||||
Lock,
|
||||
Ghost,
|
||||
Zap,
|
||||
Rocket,
|
||||
Anchor,
|
||||
Stethoscope,
|
||||
AlarmClock,
|
||||
ClipboardCheck,
|
||||
@@ -114,15 +112,6 @@ export const badgeDefs: BadgeDef[] = [
|
||||
soglie: { bronzo: 3, argento: 6, oro: 10 },
|
||||
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. */
|
||||
@@ -141,18 +130,6 @@ export const badgeSegreti: BadgeDef[] = [
|
||||
valore: (g) => (g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8 ? 1 : 0),
|
||||
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",
|
||||
nome: "Cliente VIP dell'Infermeria",
|
||||
|
||||
@@ -34,7 +34,6 @@ export type Giocatore = {
|
||||
/** Serie consecutive per tipo: si azzerano in modo indipendente. */
|
||||
serieAllenamenti: number;
|
||||
seriePartite: number;
|
||||
serieConferme: number;
|
||||
/** MVP eletti dalla squadra. */
|
||||
mvp: number;
|
||||
/** Media delle pagelle ricevute dai compagni (1-10). */
|
||||
@@ -109,7 +108,6 @@ export const giocatori: Giocatore[] = rosaCSI
|
||||
streak: 0,
|
||||
serieAllenamenti: 0,
|
||||
seriePartite: 0,
|
||||
serieConferme: 0,
|
||||
mvp: 0,
|
||||
mediaVoto: 0,
|
||||
votiPagella: 0,
|
||||
|
||||
+1
-26
@@ -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. */
|
||||
function serieSu(
|
||||
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`
|
||||
* 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
|
||||
* risponde di nuovo il cronometro riparte davvero — ha ritirato la risposta.
|
||||
*/
|
||||
|
||||
+2
-15
@@ -12,7 +12,6 @@ import { useEventi } from "./eventi";
|
||||
import {
|
||||
contaPartiteGiocate,
|
||||
contaPresenzeGiocatore,
|
||||
serieConferme,
|
||||
serieConsecutiva,
|
||||
totaliEventiGiocatore,
|
||||
useRispostePresenze,
|
||||
@@ -68,7 +67,7 @@ export function useRosa(): Giocatore[] {
|
||||
const { salvati: turniSalvati } = useTurniPalloni();
|
||||
const { infortuni, ritardi } = useInfortuniERitardi();
|
||||
const { eventi } = useEventi();
|
||||
const { presenze: mappaPresenze, tempi } = useRispostePresenze();
|
||||
const { presenze: mappaPresenze } = useRispostePresenze();
|
||||
|
||||
const votiMvp = voti.data;
|
||||
|
||||
@@ -94,7 +93,6 @@ export function useRosa(): Giocatore[] {
|
||||
streak: serieConsecutiva(g.id, eventi, mappaPresenze),
|
||||
serieAllenamenti: serieConsecutiva(g.id, eventi, mappaPresenze, "allenamento"),
|
||||
seriePartite: serieConsecutiva(g.id, eventi, mappaPresenze, "partita"),
|
||||
serieConferme: serieConferme(g.id, eventi, tempi),
|
||||
mvp: mvpVinti[g.id] ?? 0,
|
||||
mediaVoto: medie[g.id]?.media ?? 0,
|
||||
votiPagella: medie[g.id]?.voti ?? 0,
|
||||
@@ -105,18 +103,7 @@ export function useRosa(): Giocatore[] {
|
||||
infortuni: infortuni[g.id] ?? 0,
|
||||
ritardi: ritardi[g.id] ?? 0,
|
||||
}));
|
||||
}, [
|
||||
squadra,
|
||||
votiMvp,
|
||||
pagelle,
|
||||
cacche,
|
||||
turniSalvati,
|
||||
infortuni,
|
||||
ritardi,
|
||||
eventi,
|
||||
mappaPresenze,
|
||||
tempi,
|
||||
]);
|
||||
}, [squadra, votiMvp, pagelle, cacche, turniSalvati, infortuni, ritardi, eventi, mappaPresenze]);
|
||||
}
|
||||
|
||||
/** Criteri di ordinamento della classifica interna di Squadra. */
|
||||
|
||||
+2
-10
@@ -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 { Giocatore } from "./crapp-data";
|
||||
|
||||
export type SerieTipo = "allenamenti" | "partite" | "conferme";
|
||||
export type SerieTipo = "allenamenti" | "partite";
|
||||
|
||||
export type SerieDef = {
|
||||
tipo: SerieTipo;
|
||||
@@ -31,14 +31,6 @@ export const serieDefs: SerieDef[] = [
|
||||
traguardi: [2, 5, 8, 12],
|
||||
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 = {
|
||||
|
||||
@@ -90,6 +90,13 @@ function Calendario() {
|
||||
// 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 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 (
|
||||
<>
|
||||
@@ -270,13 +277,15 @@ function Calendario() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section titolo="Compleanni">
|
||||
<Section titolo="Prossimi compleanni">
|
||||
<div className="space-y-3">
|
||||
{compleanniMese.length > 0 ? (
|
||||
compleanniMese.map((c) => <EventoCard key={c.id} evento={c} />)
|
||||
{prossimiCompleanni.length > 0 ? (
|
||||
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">
|
||||
Nessun compleanno in {mesiIT[mese]!.toLowerCase()}
|
||||
{compleanniMeseCorrente.length > 0
|
||||
? "Nessun altro compleanno questo mese"
|
||||
: "Nessun compleanno questo mese"}
|
||||
</p>
|
||||
)}
|
||||
</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");
|
||||
}
|
||||
@@ -254,8 +254,8 @@ if (!locale) {
|
||||
|
||||
// --- presenze: la risposta si cambia, il cronometro no --------------------------
|
||||
// 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
|
||||
// alimenta niente. Il trigger `risposte_presenze_risposto_il_immutabile` (M9) tiene ferma
|
||||
// risposta, `aggiornato_il` è l'ultima modifica; oggi nessuna statistica le usa (la serie
|
||||
// «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.
|
||||
await prova(
|
||||
"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");
|
||||
}
|
||||
@@ -34,7 +34,6 @@ function g(valori: Partial<Giocatore> = {}): Giocatore {
|
||||
streak: 0,
|
||||
serieAllenamenti: 0,
|
||||
seriePartite: 0,
|
||||
serieConferme: 0,
|
||||
...valori,
|
||||
};
|
||||
}
|
||||
@@ -146,16 +145,6 @@ assert.equal(gradoRaggiunto(serieAllenamentiDef, 9), "argento");
|
||||
assert.equal(gradoRaggiunto(serieAllenamentiDef, 10), "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 ----------------------------------------------------------
|
||||
assert.equal(badgeGiocatore(g()).length, badgeDefs.length, "i badge normali sono sempre tutti");
|
||||
assert.ok(
|
||||
@@ -200,36 +189,6 @@ assert.deepEqual(
|
||||
["s-cacche"],
|
||||
);
|
||||
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 ---------------------------------------------------------
|
||||
const vuota = collezioneBadge(g());
|
||||
@@ -262,7 +221,6 @@ assert.equal(
|
||||
palloni: 10,
|
||||
presenze: 30,
|
||||
serieAllenamenti: 10,
|
||||
serieConferme: 15,
|
||||
}),
|
||||
),
|
||||
null,
|
||||
|
||||
@@ -19,7 +19,6 @@ function g(valori: Partial<Giocatore> = {}): Giocatore {
|
||||
streak: 0,
|
||||
serieAllenamenti: 0,
|
||||
seriePartite: 0,
|
||||
serieConferme: 0,
|
||||
...valori,
|
||||
};
|
||||
}
|
||||
@@ -57,7 +56,7 @@ assert.ok(
|
||||
// --- serie: solo sul traguardo esatto ----------------------------------------
|
||||
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({ 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% --------------------
|
||||
const obiettivo = (id: string, valore: number): ObiettivoSquadra => ({
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
contaPresenzeGiocatore,
|
||||
destinatariSollecito,
|
||||
includeInfortunato,
|
||||
serieConferme,
|
||||
serieConsecutiva,
|
||||
testoSollecito,
|
||||
totaliEventiGiocatore,
|
||||
@@ -218,91 +217,9 @@ assert.ok(
|
||||
"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 ----------------
|
||||
// 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 POI = "2026-08-08T10:00:00Z";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { aggiornaSerie, serieDefs, serieGiocatore, serieMigliore, statoSerie } from "@/lib/serie";
|
||||
|
||||
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")!;
|
||||
@@ -45,16 +45,16 @@ assert.deepEqual([fuoriScala.prossimo, fuoriScala.manca, fuoriScala.progresso],
|
||||
assert.equal(fuoriScala.messaggio, "Serie leggendaria: sei fuori scala!");
|
||||
|
||||
// --- serieGiocatore / serieMigliore ------------------------------------------
|
||||
const tutte = serieGiocatore(g({ serieAllenamenti: 4, seriePartite: 1, serieConferme: 9 }));
|
||||
assert.equal(tutte.length, serieDefs.length, "tre serie indipendenti");
|
||||
const tutte = serieGiocatore(g({ serieAllenamenti: 4, seriePartite: 6 }));
|
||||
assert.equal(tutte.length, serieDefs.length, "due serie indipendenti");
|
||||
assert.deepEqual(
|
||||
tutte.map((s) => s.valore),
|
||||
[4, 1, 9],
|
||||
[4, 6],
|
||||
"ogni serie legge il proprio contatore",
|
||||
);
|
||||
|
||||
const migliore = serieMigliore(g({ serieAllenamenti: 4, seriePartite: 1, serieConferme: 9 }));
|
||||
assert.equal(migliore.def.tipo, "conferme", "in home si mostra la serie più lunga");
|
||||
const migliore = serieMigliore(g({ serieAllenamenti: 4, seriePartite: 6 }));
|
||||
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");
|
||||
|
||||
// --- invarianti sulle definizioni --------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user