Compare commits
8
Commits
985fc2fcc1
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0a8c7971 | ||
|
|
29acb326a7 | ||
|
|
77e5daaa7a | ||
|
|
519f9aaff4 | ||
|
|
e7ae2c1884 | ||
|
|
c2c3b249f2 | ||
|
|
6b8cc33512 | ||
|
|
ff02b7439c |
No files matched your search
+4
-4
@@ -9,9 +9,9 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
|
|||||||
|
|
||||||
## Stato generale
|
## Stato generale
|
||||||
|
|
||||||
- **Versione:** 1.2.3 (in produzione su `main` la 1.2.2) — notifiche su tre canali, avvisi di palloni
|
- **Versione:** 1.3.0 (tag `v1.3.0` su `main`) — notifica di compleanno, obiettivi cliccabili con card di
|
||||||
(3 ore prima) e presenze (24 ore prima) una sola volta, niente promemoria evento, niente foto
|
dettaglio, presenze calcolate sui convocati, avvisi in Home ridisegnati, caratteri più grandi, tessera CSI
|
||||||
tessera, avviso certificato a 30 giorni per il giocatore, avviso rosso per chi non l'ha caricato.
|
tolta dalla dashboard. 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
|
||||||
@@ -20,7 +20,7 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
|
|||||||
`foto_path`: il codice che non la legge più (1.2.2) va rilasciato.
|
`foto_path`: il codice che non la legge più (1.2.2) va rilasciato.
|
||||||
- **Worker delle notifiche** (`mailer/`): in funzione su un Raspberry Pi con Docker dal 29/09/2026
|
- **Worker delle notifiche** (`mailer/`): in funzione su un Raspberry Pi con Docker dal 29/09/2026
|
||||||
([docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md)). Il codice con l'interruttore «Email» in Profilo
|
([docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md)). Il codice con l'interruttore «Email» in Profilo
|
||||||
è su `develop`, non ancora su `main`.
|
è su `main` dalla 1.3.0.
|
||||||
- **Accesso:** il login Google è l'unica via d'ingresso; i permessi di amministrazione arrivano
|
- **Accesso:** il login Google è l'unica via d'ingresso; i permessi di amministrazione arrivano
|
||||||
solo da `user_roles`. Il collegamento degli account è un processo continuo: ogni giocatore si
|
solo da `user_roles`. Il collegamento degli account è un processo continuo: ogni giocatore si
|
||||||
collega al primo accesso (DD-018).
|
collega al primo accesso (DD-018).
|
||||||
|
|||||||
@@ -85,9 +85,13 @@ quando il database non risponde.
|
|||||||
## UI
|
## UI
|
||||||
|
|
||||||
Componenti condivisi in `src/components/crapp/` (`ui-bits.tsx` per `Card`, `PageHeader`,
|
Componenti condivisi in `src/components/crapp/` (`ui-bits.tsx` per `Card`, `PageHeader`,
|
||||||
`Section`, `StatTile`), animazioni in `src/components/motion/`. Mobile-first (DD-005): poche
|
`Section`, `StatTile`; `Avviso.tsx` per gli avvisi colorati in Home; i drawer di dettaglio `BadgeDrawer` e `ObiettivoDrawer`), animazioni in `src/components/motion/`. Mobile-first (DD-005): poche
|
||||||
schermate, pochi click.
|
schermate, pochi click.
|
||||||
|
|
||||||
|
**Caratteri** (DD-049): niente testo sotto i 13 px, tranne la navigazione a pillola e i badge
|
||||||
|
da 10-11 px. Scala in uso: 13 px note, 15 px corpo, 16 px titoli di card; si usano le misure
|
||||||
|
fisse `text-[13px]`…`text-[16px]`, non `text-xs` e `text-sm`.
|
||||||
|
|
||||||
In `src/components/ui/` restano solo le due primitive shadcn davvero usate, `drawer` (vaul) e
|
In `src/components/ui/` restano solo le due primitive shadcn davvero usate, `drawer` (vaul) e
|
||||||
`sonner`: le altre 43 non erano importate da nessuna parte (DD-021). Il resto dell'interfaccia
|
`sonner`: le altre 43 non erano importate da nessuna parte (DD-021). Il resto dell'interfaccia
|
||||||
è composto con Tailwind e la primitiva `Card`, che è l'unica definizione di raggio, sfondo e
|
è composto con Tailwind e la primitiva `Card`, che è l'unica definizione di raggio, sfondo e
|
||||||
|
|||||||
@@ -13,12 +13,48 @@ versioni `0.y.z` erano pre-release.
|
|||||||
|
|
||||||
## [Non rilasciato]
|
## [Non rilasciato]
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-10-06
|
||||||
|
|
||||||
|
Gli auguri di compleanno arrivano da soli, gli obiettivi di squadra si possono toccare per capire come funzionano
|
||||||
|
e le presenze si contano solo sugli eventi a cui si è convocati. In tutta l'app i caratteri sono più grandi e gli
|
||||||
|
avvisi in Home più chiari. La migration `m31_notifica_compleanno` è **da applicare in produzione** (`npx supabase
|
||||||
|
db push`); il resto non tocca il database.
|
||||||
|
|
||||||
### Aggiunto
|
### Aggiunto
|
||||||
|
|
||||||
- **Notifica di compleanno** — ogni giorno, dalle 8:00, il festeggiato riceve un messaggio di auguri e tutti gli altri membri
|
- **Notifica di compleanno** — ogni giorno, dalle 8:00, il festeggiato riceve un messaggio di auguri e tutti gli altri membri
|
||||||
attivi, allenatori compresi, il solo avviso «Oggi è il compleanno di …»; con più compleanni lo stesso giorno un solo
|
attivi, allenatori compresi, il solo avviso «Oggi è il compleanno di …»; con più compleanni lo stesso giorno un solo
|
||||||
messaggio a testa. Push, centro notifiche ed email. Il 29 febbraio non è gestito (DD-047, issue #11). Migration
|
messaggio a testa. Push, centro notifiche ed email. Il 29 febbraio non è gestito (DD-047, issue #11). Migration
|
||||||
`m31_notifica_compleanno`, da applicare in produzione.
|
`m31_notifica_compleanno`, da applicare in produzione.
|
||||||
|
- **Obiettivi cliccabili** — toccando un obiettivo (in Squadra o nella card della Home) si apre una card che
|
||||||
|
spiega tutto: come si calcola, cosa conta e cosa no, cosa è una presenza persa, periodo di validità, fonte dei dati,
|
||||||
|
un esempio con numeri. Nessuna migration.
|
||||||
|
|
||||||
|
### Modificato
|
||||||
|
|
||||||
|
- **Obiettivo «90% di presenze del mese»** — i posti sono i convocati di ogni partita e allenamento (tutta la rosa se
|
||||||
|
non ce ne sono), come nelle presenze collettive: chi non è convocato non abbassa più la percentuale. Assente, forse,
|
||||||
|
infortunato e nessuna risposta restano presenze perse. Nessuna migration.
|
||||||
|
- **Obiettivo «Presenze collettive»** — sostituisce «250 presenze complessive»: è la percentuale di presenze di tutta la
|
||||||
|
rosa su allenamenti e partite definiti in stagione (passati e futuri, posti = convocati), con target 90%. Nessuna migration.
|
||||||
|
- **Obiettivo «1 evento di squadra al mese»** — si completa quando arriva l'ora dell'evento, non appena viene
|
||||||
|
creato. Un evento futuro non conta finché la sua data e ora non sono passate. Nessuna migration.
|
||||||
|
- **Avvisi in Home più leggibili** — certificati e turno palloni usano lo stesso componente (`Avviso`): icona
|
||||||
|
propria per ogni caso (non solo il colore), titolo e testo più grandi, elenchi di persone in righe con il dettaglio
|
||||||
|
a destra e il conteggio, freccia dove la card porta ai documenti, comparsa graduale. Il turno palloni ha la pillola
|
||||||
|
«Oggi»/«In arrivo» e i messaggi in riquadri. Solo aspetto: testi e regole di quando compaiono non cambiano.
|
||||||
|
Nessuna migration.
|
||||||
|
- **Caratteri più grandi in tutta l'app** — ogni testo sotto i 16 px sale di un passo (12→13, 14→15, ecc.), nelle
|
||||||
|
card, negli elenchi, nelle statistiche e nei drawer. Restano uguali le barre a pillola dei menu (navigazione in
|
||||||
|
basso e sottosezioni) e i badge minuscoli. Solo aspetto, nessuna migration (DD-049).
|
||||||
|
- **Etichetta «Cacche» nella rosa** — nella scheda del giocatore in Squadra la riga «Cacche/partita» si chiama
|
||||||
|
«Cacche»; il valore resta la media per partita. Nessuna migration.
|
||||||
|
|
||||||
|
### Rimosso
|
||||||
|
|
||||||
|
- **Tessera CSI dalla dashboard** — in Profili spariscono il blocco «Tesseramento CSI» (numero e data
|
||||||
|
tessera), il badge «Tesserato»/«Da tesserare» e il riquadro «Tesserati» della tab Squadra. L'export CSV non
|
||||||
|
cambia. Le colonne `numero_tessera` e `data_tessera` restano a database, non più usate (DD-048). Nessuna migration.
|
||||||
|
|
||||||
## [1.2.3] - 2026-10-04
|
## [1.2.3] - 2026-10-04
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ Colonne di `giocatori_squadra` che meritano una nota:
|
|||||||
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `email` | Chiave del collegamento automatico account↔giocatore al primo accesso (DD-018). Impostabile da `/admin`. |
|
| `email` | Chiave del collegamento automatico account↔giocatore al primo accesso (DD-018). Impostabile da `/admin`. |
|
||||||
| `auth_user_id` | Collega lo slot all'account; NULL finché nessuno lo reclama. |
|
| `auth_user_id` | Collega lo slot all'account; NULL finché nessuno lo reclama. |
|
||||||
| `numero_tessera`, `data_tessera` | Chi è già tesserato al CSI. Come `numero` e `ruolo` le scrive solo un admin: il trigger le include tra i campi bloccati per chi reclama il proprio slot. |
|
| `numero_tessera`, `data_tessera` | Non più usate dall'interfaccia (DD-048): i valori già inseriti restano, nessuna schermata li mostra o li scrive. Il trigger le include ancora tra i campi bloccati per chi reclama il proprio slot. |
|
||||||
| `nascita` | Pubblica a tutta la rosa (DD-031). Non si scrive mai da qui: il trigger `sincronizza_nascita_pubblica` su `profili_giocatore` la tiene allineata a `data_nascita` (impostata, aggiornata o azzerata). |
|
| `nascita` | Pubblica a tutta la rosa (DD-031). Non si scrive mai da qui: il trigger `sincronizza_nascita_pubblica` su `profili_giocatore` la tiene allineata a `data_nascita` (impostata, aggiornata o azzerata). |
|
||||||
| `tipo` | `giocatore` o `allenatore` (DD-034). L'allenatore ha uno slot per collegarsi e per i dati personali ma è fuori dalla rosa di gioco (`inRosa()`), con `numero` NULL (vincolo `giocatori_squadra_numero_giocatori`) e `ruolo` vuoto. Lo scrive solo un admin. |
|
| `tipo` | `giocatore` o `allenatore` (DD-034). L'allenatore ha uno slot per collegarsi e per i dati personali ma è fuori dalla rosa di gioco (`inRosa()`), con `numero` NULL (vincolo `giocatori_squadra_numero_giocatori`) e `ruolo` vuoto. Lo scrive solo un admin. |
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -29,8 +29,8 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`).
|
|||||||
- [x] Profilo Giocatore — dati personali, documento d'identità, certificato medico
|
- [x] Profilo Giocatore — dati personali, documento d'identità, certificato medico
|
||||||
(caricamento, scadenza, stato, download); la foto tessera è stata tolta (DD-044); lo storico dei certificati
|
(caricamento, scadenza, stato, download); la foto tessera è stata tolta (DD-044); lo storico dei certificati
|
||||||
resta un'estensione futura
|
resta un'estensione futura
|
||||||
- [x] Gestione tesseramenti CSI — raccolta dati, export CSV e tracciamento di chi è già
|
- [x] Gestione tesseramenti CSI — raccolta dati ed export CSV; il tracciamento di numero e data
|
||||||
tesserato (numero e data di tessera)
|
di tessera è stato tolto dalla dashboard (DD-048)
|
||||||
- [x] Collegamento CSI (stagione 2025/26)
|
- [x] Collegamento CSI (stagione 2025/26)
|
||||||
- [x] Classifica automatica (campionato e Coppa)
|
- [x] Classifica automatica (campionato e Coppa)
|
||||||
- [x] Risultati campionato
|
- [x] Risultati campionato
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# DD-048 — Via numero e data tessera dalla dashboard
|
||||||
|
|
||||||
|
**Data:** 6 ottobre 2026
|
||||||
|
**Stato:** Accettata (nessuna migration)
|
||||||
|
|
||||||
|
**Contesto**
|
||||||
|
La scheda di ogni giocatore in `/admin` aveva un blocco «Tesseramento CSI» con numero e data
|
||||||
|
tessera, scritti solo dall'admin (roadmap v1.1). Non serve più tenerli nell'app.
|
||||||
|
|
||||||
|
**Decisione**
|
||||||
|
|
||||||
|
1. Sparisce il blocco «Tesseramento CSI» dalla scheda del giocatore, con il pulsante «Salva
|
||||||
|
tesseramento» e il salvataggio (`useSalvaTesseramento()`).
|
||||||
|
2. Spariscono anche le due viste che ne dipendevano e senza il campo non avrebbero più senso:
|
||||||
|
il badge «Tesserato»/«Da tesserare» sulla scheda e il riquadro «Tesserati» della tab Squadra.
|
||||||
|
3. **Restano** l'export CSV per il tesseramento (non conteneva la tessera) e il resto del profilo
|
||||||
|
amministrativo: documento, certificato, dati personali.
|
||||||
|
4. **Dati:** le colonne `giocatori_squadra.numero_tessera` e `data_tessera` non vengono toccate
|
||||||
|
e continuano a essere lette dal codice, ma nessuna schermata le mostra né le scrive. I valori
|
||||||
|
già inseriti restano a database.
|
||||||
|
|
||||||
|
**Alternative scartate**
|
||||||
|
|
||||||
|
- Eliminare anche le colonne → scartata per ora: serve una migration e una decisione sui valori
|
||||||
|
già inseriti; si può fare dopo senza toccare l'interfaccia.
|
||||||
|
- Lasciare badge e riquadro → scartata: senza il campo resterebbero fermi su «Da tesserare» e
|
||||||
|
`0/N`, un'informazione falsa.
|
||||||
|
|
||||||
|
**Conseguenze**
|
||||||
|
|
||||||
|
- L'admin non può più registrare né correggere la tessera dall'app.
|
||||||
|
- Chi lavora sul codice trova `numeroTessera` e `dataTessera` nel tipo `GiocatoreSquadra` senza
|
||||||
|
uso in interfaccia: sono volutamente lasciati finché le colonne esistono.
|
||||||
|
|
||||||
|
**Riesame**
|
||||||
|
Se tra qualche mese nessuno ha bisogno dei valori già inseriti, eliminare le colonne con una
|
||||||
|
migration e togliere i due campi dal tipo e dalla lettura.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# DD-049 — Caratteri più grandi in tutta l'app
|
||||||
|
|
||||||
|
**Data:** 6 ottobre 2026
|
||||||
|
**Stato:** Accettata (solo interfaccia, nessuna migration)
|
||||||
|
|
||||||
|
**Contesto**
|
||||||
|
L'app si usa soprattutto dal telefono, anche da chi non è pratico. Molti testi erano a 12 px
|
||||||
|
(`text-xs`) e 14 px (`text-sm`): faticosi da leggere, soprattutto in Home, dove le card di
|
||||||
|
avvisi, eventi e statistiche hanno quasi solo testo piccolo.
|
||||||
|
|
||||||
|
**Decisione**
|
||||||
|
|
||||||
|
1. Tutti i testi sotto i 16 px salgono di **un passo**, in ogni pagina:
|
||||||
|
|
||||||
|
| Prima | Dopo |
|
||||||
|
| ----------------- | ----- |
|
||||||
|
| `text-xs` (12 px) | 13 px |
|
||||||
|
| 13 px | 14 px |
|
||||||
|
| 14 px | 15 px |
|
||||||
|
| `text-sm` (14 px) | 15 px |
|
||||||
|
| 15 px | 16 px |
|
||||||
|
|
||||||
|
Si usano le misure fisse `text-[13px]`, `text-[14px]`, `text-[15px]`, `text-[16px]`, già
|
||||||
|
presenti nel codice. `text-base` (16 px) e i titoli grandi non cambiano.
|
||||||
|
|
||||||
|
2. **Restano invariati** i testi delle barre a pillola dei menu, cioè la navigazione in basso
|
||||||
|
(`BottomNav`) e le sottosezioni (`BarraSottosezioni`), e i testi minuscoli da 10 e 11 px
|
||||||
|
(conteggio sulla campanella).
|
||||||
|
3. **Avvisi in Home** (certificati e turno palloni): un solo componente, `Avviso`
|
||||||
|
(`src/components/crapp/Avviso.tsx`), con titolo da 18 px, testi da 15 px, icona propria per
|
||||||
|
ogni caso (il colore non è l'unico segnale), elenchi in righe con il dettaglio a destra. Il
|
||||||
|
turno palloni ha in più la pillola «Oggi» o «In arrivo» e i messaggi in riquadri. Cambia solo
|
||||||
|
l'aspetto, non i testi né le regole su quando un avviso compare.
|
||||||
|
|
||||||
|
**Alternative scartate**
|
||||||
|
|
||||||
|
- Alzare la dimensione base del browser (`rem`) → scartata: ingrandirebbe anche icone, spazi e
|
||||||
|
barre a pillola, e ogni schermata andrebbe ricontrollata.
|
||||||
|
- Ingrandire solo la Home → scartata: le altre pagine avevano gli stessi testi piccoli.
|
||||||
|
- Salire di due passi → scartata: in contenitori stretti (badge, tessere, righe di classifica)
|
||||||
|
il testo andrebbe a capo o verrebbe troncato.
|
||||||
|
|
||||||
|
**Conseguenze**
|
||||||
|
|
||||||
|
- Un testo che stava stretto può andare a capo o troncarsi: non c'è un controllo visivo
|
||||||
|
automatico, va guardato sul telefono. Chi vede un punto rotto segnala la schermata.
|
||||||
|
- Per un testo nuovo, la scala di partenza è 13 px per le note, 15 px per il corpo, 16 px per i
|
||||||
|
titoli di card.
|
||||||
|
- Le card degli eventi sono condivise tra Home e Calendario: crescono insieme.
|
||||||
|
|
||||||
|
**Riesame**
|
||||||
|
Se dopo l'uso sul telefono compaiono testi troncati in più punti, tornare a un passo solo nei
|
||||||
|
contenitori stretti, senza toccare il resto.
|
||||||
@@ -86,6 +86,8 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand
|
|||||||
| [DD-038](DD-038.md) | Tre canali per ogni notifica |
|
| [DD-038](DD-038.md) | Tre canali per ogni notifica |
|
||||||
| [DD-039](DD-039.md) | Niente promemoria all'allenatore |
|
| [DD-039](DD-039.md) | Niente promemoria all'allenatore |
|
||||||
| [DD-040](DD-040.md) | Palloni e solleciti automatici |
|
| [DD-040](DD-040.md) | Palloni e solleciti automatici |
|
||||||
|
| [DD-049](DD-049.md) | Caratteri più grandi in tutta l'app |
|
||||||
|
| [DD-048](DD-048.md) | Via la tessera dalla dashboard |
|
||||||
| [DD-047](DD-047.md) | Notifica di compleanno |
|
| [DD-047](DD-047.md) | Notifica di compleanno |
|
||||||
| [DD-046](DD-046.md) | Avviso certificati mancanti |
|
| [DD-046](DD-046.md) | Avviso certificati mancanti |
|
||||||
| [DD-045](DD-045.md) | Sollecito presenze: un solo avviso a 24 ore |
|
| [DD-045](DD-045.md) | Sollecito presenze: un solo avviso a 24 ore |
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ nessuna schermata:
|
|||||||
| Dove | Cosa si nasconde all'allenatore |
|
| Dove | Cosa si nasconde all'allenatore |
|
||||||
| ----------------------------------- | -------------------------------------------------------------------------------- |
|
| ----------------------------------- | -------------------------------------------------------------------------------- |
|
||||||
| `/squadra` → Stats | il criterio di ordinamento «Cacche» (`cacchePartita`) |
|
| `/squadra` → Stats | il criterio di ordinamento «Cacche» (`cacchePartita`) |
|
||||||
| `/squadra` → Rosa, scheda giocatore | la riga «Cacche/partita 💩» |
|
| `/squadra` → Rosa, scheda giocatore | la riga «Cacche 💩» |
|
||||||
| `/partita/$id` | la sezione «Badge votati dai compagni» e il sondaggio cacche (`SondaggioCacche`) |
|
| `/partita/$id` | la sezione «Badge votati dai compagni» e il sondaggio cacche (`SondaggioCacche`) |
|
||||||
| push «Sondaggio pre-partita aperto» | non parte verso i dispositivi degli allenatori (`apri-sondaggio.ts`) |
|
| push «Sondaggio pre-partita aperto» | non parte verso i dispositivi degli allenatori (`apri-sondaggio.ts`) |
|
||||||
| `/profilo`, `/squadra` | le tab Badge (già escluse sopra) |
|
| `/profilo`, `/squadra` | le tab Badge (già escluse sopra) |
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
**Stato:** implementato — mesi/scadenze dinamici, target stagionali fissi da rivedere a mano,
|
**Stato:** implementato — mesi/scadenze dinamici, target stagionali fissi da rivedere a mano,
|
||||||
copertura test completa (unit + integration) su tutti e 10 gli obiettivi.
|
copertura test completa (unit + integration) su tutti e 10 gli obiettivi.
|
||||||
**File principali:** `src/lib/obiettivi.ts`, `src/lib/rosa.ts` (`useObiettivi()`)
|
**File principali:** `src/lib/obiettivi.ts`, `src/lib/rosa.ts` (`useObiettivi()`),
|
||||||
|
`src/components/crapp/ObiettivoDrawer.tsx` (card di dettaglio)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,22 +38,27 @@ corrente dinamico (vedi sotto).
|
|||||||
|
|
||||||
## Obiettivi definiti
|
## Obiettivi definiti
|
||||||
|
|
||||||
| id | Obiettivo | Calcolo | Target | Fonte |
|
| id | Obiettivo | Calcolo | Target | Fonte |
|
||||||
| ----- | ---------------------------------- | ----------------------------------------------------------------------------- | --------------------- | ----------------------------- |
|
| ----- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------- |
|
||||||
| `o1` | 90% presenze del mese | risposte presente/ritardo su partite+allenamenti del mese corrente (dinamico) | 90% | `risposte_presenze` |
|
| `o1` | 90% presenze del mese | presente/ritardo su partite+allenamenti del mese corrente; posti = convocati dell'evento, tutta la rosa se vuoto (dinamico) | 90% | `risposte_presenze` |
|
||||||
| `o2` | Tutti rispondono alle convocazioni | risposte totali / eventi possibili (esclusi i compleanni) | 90% | `risposte_presenze` |
|
| `o2` | Tutti rispondono alle convocazioni | risposte totali / eventi possibili (esclusi i compleanni) | 90% | `risposte_presenze` |
|
||||||
| `o7` | 250 presenze complessive | somma presenze di tutta la rosa, stagione intera | 250 | aggregato da `useRosa()` |
|
| `o7` | Presenze collettive | % di presenze (presente/ritardo) su tutti gli allenamenti e le partite definiti, passati e futuri; posti = convocati dell'evento (tutta la rosa se vuoto) | 90 | `eventi_app` + `risposte_presenze` |
|
||||||
| `o12` | Media pagelle da 7.5 | media di tutti i voti, arrotondata a una cifra decimale | 7.5 | `pagelle_voti` |
|
| `o12` | Media pagelle da 7.5 | media di tutti i voti, arrotondata a una cifra decimale | 7.5 | `pagelle_voti` |
|
||||||
| `o13` | 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
| `o13` | 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` |
|
||||||
| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` |
|
| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` |
|
||||||
| `o3` | Prima vittoria del campionato | `min(vittorie, 1)` | 1 | JSON partite CSI (vedi sotto) |
|
| `o3` | Prima vittoria del campionato | `min(vittorie, 1)` | 1 | JSON partite CSI (vedi sotto) |
|
||||||
| `o4` | 5 vittorie in campionato | `min(vittorie, 5)` | 5 | JSON partite CSI |
|
| `o4` | 5 vittorie in campionato | `min(vittorie, 5)` | 5 | JSON partite CSI |
|
||||||
| `o5` | 10 vittorie in campionato | `min(vittorie, 10)` | 10 | JSON partite CSI |
|
| `o5` | 10 vittorie in campionato | `min(vittorie, 10)` | 10 | JSON partite CSI |
|
||||||
| `o6` | 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente (dinamico) | 1 | `eventi_app` |
|
| `o6` | 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente la cui data e ora sono già passate (dinamico) | 1 | `eventi_app` |
|
||||||
|
|
||||||
Mostrati in `squadra.tsx` (elenco completo con barra di progresso) e in `index.tsx` (home: il
|
Mostrati in `squadra.tsx` (elenco completo con barra di progresso) e in `index.tsx` (home: il
|
||||||
primo obiettivo non completato). Un obiettivo che supera il 90% genera anche una notifica
|
primo obiettivo non completato).
|
||||||
smart (`notifiche-smart.ts`). I target fissi (250 presenze, 200 pagelle, 7.5 di media, 1/5/10
|
Ogni obiettivo è **cliccabile**: apre la card di dettaglio (`ObiettivoDrawer`, stesso drawer dei
|
||||||
|
badge) con stato attuale, come si calcola, cosa conta e cosa no, quando vale, da dove arrivano i
|
||||||
|
dati e un esempio con numeri. Per le due percentuali di presenze (`o1`, `o7`) la card distingue «Presenza persa» (assente, forse, infortunato, nessuna risposta: il posto resta vuoto) da «Non entra nel calcolo» (cene, eventi di squadra, compleanni). I testi stanno nel campo `dettaglio` di ogni obiettivo in
|
||||||
|
`obiettivi.ts`: chi cambia il calcolo o il target di un obiettivo aggiorna anche il suo
|
||||||
|
`dettaglio`, altrimenti la card mente. Un obiettivo che supera il 90% genera anche una notifica
|
||||||
|
smart (`notifiche-smart.ts`). I target fissi (200 pagelle, 7.5 di media, 1/5/10
|
||||||
vittorie) sono scelte editoriali da rivedere a mano a ogni stagione — nessuna configurazione o
|
vittorie) sono scelte editoriali da rivedere a mano a ogni stagione — nessuna configurazione o
|
||||||
UI per farlo, si cambia il numero in `obiettivi.ts`. Fa eccezione "Continuità di squadra"
|
UI per farlo, si cambia il numero in `obiettivi.ts`. Fa eccezione "Continuità di squadra"
|
||||||
(vedi sotto): il suo target ha un significato specifico, non va scalato come gli altri.
|
(vedi sotto): il suo target ha un significato specifico, non va scalato come gli altri.
|
||||||
@@ -65,7 +71,22 @@ UI per farlo, si cambia il numero in `obiettivi.ts`. Fa eccezione "Continuità d
|
|||||||
automaticamente a ogni cambio mese: il mese di riferimento è calcolato dalla data corrente
|
automaticamente a ogni cambio mese: il mese di riferimento è calcolato dalla data corrente
|
||||||
(fuso Europe/Rome, `meseCorrente(oggi)`), non più una costante fissa. Per `o1`, titolo
|
(fuso Europe/Rome, `meseCorrente(oggi)`), non più una costante fissa. Per `o1`, titolo
|
||||||
("90% di presenze ad agosto" / "a settembre" / ...) e scadenza (ultimo giorno del mese)
|
("90% di presenze ad agosto" / "a settembre" / ...) e scadenza (ultimo giorno del mese)
|
||||||
seguono di conseguenza.
|
seguono di conseguenza. I posti di ogni partita e allenamento sono i convocati (tutta la rosa se non ce
|
||||||
|
ne sono): chi non è convocato non pesa, mentre assente, forse, infortunato e nessuna risposta sono
|
||||||
|
presenze perse.
|
||||||
|
|
||||||
|
`o6` si completa quando **arriva l'ora dell'evento**, non quando lo si definisce: conta solo
|
||||||
|
un evento di tipo "evento" del mese corrente il cui `data` + `ora` (fuso Europe/Rome,
|
||||||
|
`adessoRoma(oggi)`) è già passato; senza ora vale `00:00`. Un evento futuro lascia l'obiettivo a
|
||||||
|
0/1 finché non scatta l'ora. Il valore si ricalcola al render: con l'app già aperta il
|
||||||
|
passaggio a completato compare al refresh successivo.
|
||||||
|
|
||||||
|
`o7` ("Presenze collettive") invece **non** si azzera: copre tutta la stagione, cioè tutti gli
|
||||||
|
allenamenti e le partite in calendario, passati e futuri (le risposte sugli eventi futuri sono
|
||||||
|
conferme). Differisce da `o1` solo per il periodo (stagione, non mese): anche in `o1` i posti sono i
|
||||||
|
convocati di ciascun evento, così chi non è convocato non pesa; con la stessa funzione
|
||||||
|
`percentualePresenze()`. Non c'è un confine di stagione esplicito: conta ciò
|
||||||
|
che c'è in `eventi_app`.
|
||||||
|
|
||||||
`o2` ("Tutti rispondono alle convocazioni") non si azzera — aggrega su tutti gli eventi in
|
`o2` ("Tutti rispondono alle convocazioni") non si azzera — aggrega su tutti gli eventi in
|
||||||
programma, non solo quelli del mese corrente — ma la sua `scadenza` mostrata in interfaccia è
|
programma, non solo quelli del mese corrente — ma la sua `scadenza` mostrata in interfaccia è
|
||||||
@@ -144,21 +165,29 @@ Dettagli completi (endpoint, identificativi di stagione, altri limiti del colleg
|
|||||||
Tutti e 10 gli obiettivi hanno unit test **e** integration test end-to-end (dati scritti/letti
|
Tutti e 10 gli obiettivi hanno unit test **e** integration test end-to-end (dati scritti/letti
|
||||||
da un backend reale, non solo funzione pura con contesto costruito a mano).
|
da un backend reale, non solo funzione pura con contesto costruito a mano).
|
||||||
|
|
||||||
| Obiettivi | Unit test | Integration test |
|
| Obiettivi | Unit test | Integration test |
|
||||||
| ---------- | ----------------------------- | -------------------------------------------------------- |
|
| ---------- | ----------------------------- | -------------------------------------------------------------------------------- |
|
||||||
| o1, o2, o6 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) |
|
| o1, o2, o6 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale, o1 anche con i convocati) |
|
||||||
| o7 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) |
|
| o7 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) |
|
||||||
| o11 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) |
|
| o11 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) |
|
||||||
| o12, o13 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) |
|
| o12, o13 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) |
|
||||||
| o3, o4, o5 | `test/unit/obiettivi.test.ts` | `test/integration/api.test.ts` (CSI reale in produzione) |
|
| o3, o4, o5 | `test/unit/obiettivi.test.ts` | `test/integration/api.test.ts` (CSI reale in produzione) |
|
||||||
|
|
||||||
|
- **Presenze (o1, o7), casi limite** (`test/unit/obiettivi.test.ts`): solo partite e allenamenti
|
||||||
|
contano, assente/forse/infortunato/nessuna risposta sono posti vuoti, il non convocato non pesa,
|
||||||
|
confine di mese nel fuso Europe/Rome, convocato fuori rosa, rosa vuota, arrotondamento e soglia
|
||||||
|
del 90%. Verificato anche con mutazioni (posti = tutta la rosa; eventi di squadra contati): il test
|
||||||
|
diventa rosso.
|
||||||
|
- **Card di dettaglio** (`test/unit/obiettivi-dettaglio.test.ts`): ogni obiettivo ha tutti i testi,
|
||||||
|
«Presenza persa» solo su o1/o7, la card renderizzata mostra tutte le sezioni e lo stato
|
||||||
|
in corso/completato.
|
||||||
- **o1/o2/o6** (Supabase locale): scrive eventi e risposte veri su `eventi_app`/
|
- **o1/o2/o6** (Supabase locale): scrive eventi e risposte veri su `eventi_app`/
|
||||||
`risposte_presenze`, li rilegge con `leggiEventi()` (la stessa funzione server dell'app) e una
|
`risposte_presenze`, li rilegge con `leggiEventi()` (la stessa funzione server dell'app) e una
|
||||||
query REST equivalente a `fetchPresenze()`. Copre: contesto vuoto, aggregazione su più eventi,
|
query REST equivalente a `fetchPresenze()`. Copre: contesto vuoto, aggregazione su più eventi,
|
||||||
filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico
|
filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico
|
||||||
(evento dentro/fuori mese), scadenza dinamica.
|
(evento dentro/fuori mese), scadenza dinamica.
|
||||||
- **o7** (Supabase locale): scrive eventi/presenze reali, calcola `contaPresenzeGiocatore()` (la
|
- **o7** (Supabase locale): scrive un allenamento e una partita con convocati e presenze reali,
|
||||||
stessa funzione pura usata da `useRosa()` in produzione) sui dati riletti, verifica la somma.
|
li rilegge con `leggiEventi()`/`leggiPresenze()` e verifica la percentuale (il ritardo conta).
|
||||||
- **o11** (Supabase locale): scrive tre allenamenti e presenze reali, calcola
|
- **o11** (Supabase locale): scrive tre allenamenti e presenze reali, calcola
|
||||||
`serieConsecutiva()` sui dati riletti, verifica che solo chi resta in serie venga contato.
|
`serieConsecutiva()` sui dati riletti, verifica che solo chi resta in serie venga contato.
|
||||||
- **o12/o13** (Supabase locale): scrive voti veri su `pagelle_voti` rispettando i vincoli reali
|
- **o12/o13** (Supabase locale): scrive voti veri su `pagelle_voti` rispettando i vincoli reali
|
||||||
|
|||||||
@@ -328,7 +328,6 @@ Per ogni giocatore, nella tab Profili, vengono mostrati.
|
|||||||
- Stato del profilo
|
- Stato del profilo
|
||||||
- Certificato medico
|
- Certificato medico
|
||||||
- Documento di identità
|
- Documento di identità
|
||||||
- Stato tesseramento CSI (tesserato / da tesserare)
|
|
||||||
|
|
||||||
Azioni disponibili.
|
Azioni disponibili.
|
||||||
|
|
||||||
@@ -336,7 +335,6 @@ Azioni disponibili.
|
|||||||
- Scarica certificato
|
- Scarica certificato
|
||||||
- Scarica documento
|
- Scarica documento
|
||||||
- Modifica dati squadra e dati personali del giocatore (DD-017)
|
- Modifica dati squadra e dati personali del giocatore (DD-017)
|
||||||
- Registra numero e data della tessera CSI, una volta arrivata dal comitato
|
|
||||||
- Scollega account, per liberare uno slot assegnato per errore
|
- Scollega account, per liberare uno slot assegnato per errore
|
||||||
- Aggiungi giocatore, per inserire un nuovo membro della squadra
|
- Aggiungi giocatore, per inserire un nuovo membro della squadra
|
||||||
- Disattiva/Riattiva giocatore, per chi lascia la squadra (o rientra)
|
- Disattiva/Riattiva giocatore, per chi lascia la squadra (o rientra)
|
||||||
@@ -362,12 +360,9 @@ Campi esportati.
|
|||||||
|
|
||||||
## Tracciamento tesseramento
|
## Tracciamento tesseramento
|
||||||
|
|
||||||
Numero e data della tessera CSI non sono dati che il giocatore conosce in anticipo: arrivano
|
Tolto dalla dashboard (DD-048): numero e data della tessera CSI non si registrano più dall'app e non
|
||||||
dal comitato dopo l'iscrizione effettiva. Per questo, a differenza dei dati personali del
|
ci sono più il badge «Tesserato»/«Da tesserare» né il conteggio dei tesserati. Le colonne
|
||||||
profilo, li scrive solo un amministratore — come nome, cognome, numero di maglia e ruolo
|
`numero_tessera` e `data_tessera` esistono ancora a database, senza uso.
|
||||||
(DD-017), il trigger sulla tabella li rende non modificabili dal giocatore stesso. La
|
|
||||||
dashboard mostra un badge "Tesserato"/"Da tesserare" su ogni scheda e il conteggio
|
|
||||||
complessivo della squadra.
|
|
||||||
|
|
||||||
## Completamento profilo
|
## Completamento profilo
|
||||||
|
|
||||||
|
|||||||
@@ -48,20 +48,19 @@ gli allenatori (`tipo = 'allenatore'`, DD-034): hanno uno slot ma non giocano.
|
|||||||
Nella tab Rosa gli allenatori attivi compaiono in cima, con foto, nome e la dicitura
|
Nella tab Rosa gli allenatori attivi compaiono in cima, con foto, nome e la dicitura
|
||||||
«Allenatore» dove i giocatori hanno il ruolo in campo (`ruoloVisibile()`); niente numero,
|
«Allenatore» dove i giocatori hanno il ruolo in campo (`ruoloVisibile()`); niente numero,
|
||||||
statistiche né badge, e niente posto nella tab Stats. Chi usa l'app da allenatore vede solo
|
statistiche né badge, e niente posto nella tab Stats. Chi usa l'app da allenatore vede solo
|
||||||
Rosa e Stats, senza badge e senza cacche (criterio «Cacche» e riga «Cacche/partita»). La
|
Rosa e Stats, senza badge e senza cacche (criterio «Cacche» e riga «Cacche»). La
|
||||||
specifica completa sta in [allenatore.md](allenatore.md).
|
specifica completa sta in [allenatore.md](allenatore.md).
|
||||||
|
|
||||||
## Gestione dati squadra (solo amministratore)
|
## Gestione dati squadra (solo amministratore)
|
||||||
|
|
||||||
Da `/admin` un amministratore può ([DD-017](../decisions/DD-017.md)):
|
Da `/admin` un amministratore può ([DD-017](../decisions/DD-017.md)):
|
||||||
|
|
||||||
| Azione | Hook | Effetto |
|
| Azione | Hook | Effetto |
|
||||||
| ------------------------------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| Modificare dati squadra | `useSalvaDatiSquadra()` | Nome, cognome, numero, ruolo, email (usata per il collegamento automatico, non il dato personale del profilo) |
|
| Modificare dati squadra | `useSalvaDatiSquadra()` | Nome, cognome, numero, ruolo, email (usata per il collegamento automatico, non il dato personale del profilo) |
|
||||||
| Aggiungere un giocatore | `useAggiungiGiocatore()` | Nuova riga con id progressivo `g<N>` (`prossimoIdGiocatore()`), non generato dal database; con tipo «Allenatore» senza numero né ruolo (DD-034) |
|
| Aggiungere un giocatore | `useAggiungiGiocatore()` | Nuova riga con id progressivo `g<N>` (`prossimoIdGiocatore()`), non generato dal database; con tipo «Allenatore» senza numero né ruolo (DD-034) |
|
||||||
| Attivare/disattivare | `useImpostaAttivo()` | Non elimina la riga: la storia della stagione resta intatta |
|
| Attivare/disattivare | `useImpostaAttivo()` | Non elimina la riga: la storia della stagione resta intatta |
|
||||||
| Scollegare un account | `useScollegaAccount()` | Libera uno slot collegato per errore ([DD-016](../decisions/DD-016.md) regola 2); il giocatore si ricollega al primo accesso successivo |
|
| Scollegare un account | `useScollegaAccount()` | Libera uno slot collegato per errore ([DD-016](../decisions/DD-016.md) regola 2); il giocatore si ricollega al primo accesso successivo |
|
||||||
| Registrare il tesseramento CSI | `useSalvaTesseramento()` | Numero e data tessera, note solo dopo il tesseramento effettivo (vedi `profilo-giocatore.md`) |
|
|
||||||
|
|
||||||
Il collegamento giocatore↔account, invece, non è manuale: avviene in automatico al primo
|
Il collegamento giocatore↔account, invece, non è manuale: avviene in automatico al primo
|
||||||
accesso con Google, per corrispondenza email
|
accesso con Google, per corrispondenza email
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "crapp",
|
"name": "crapp",
|
||||||
"version": "1.2.3",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "crapp",
|
"name": "crapp",
|
||||||
"version": "1.2.3",
|
"version": "1.3.0",
|
||||||
"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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "crapp",
|
"name": "crapp",
|
||||||
"version": "1.2.3",
|
"version": "1.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||||
|
import { Reveal } from "@/components/motion/Reveal";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gravità di un avviso in Home. Il colore non è l'unico segnale: ogni avviso ha anche un'icona
|
||||||
|
* sua e un titolo, così si distingue anche senza vedere i colori.
|
||||||
|
*/
|
||||||
|
export type TonoAvviso = "critico" | "attenzione" | "scaduto" | "azione";
|
||||||
|
|
||||||
|
const toni: Record<TonoAvviso, string> = {
|
||||||
|
critico: "bg-destructive text-destructive-foreground",
|
||||||
|
attenzione: "bg-warning text-warning-foreground",
|
||||||
|
scaduto: "bg-primary text-primary-foreground",
|
||||||
|
azione: "bg-accent-grad text-accent-foreground",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Una riga di un elenco: a sinistra chi, a destra il dettaglio (es. la scadenza). */
|
||||||
|
export type RigaAvviso = { chi: string; cosa: string };
|
||||||
|
|
||||||
|
type ProprietaCorpo = {
|
||||||
|
tono: TonoAvviso;
|
||||||
|
icona: LucideIcon;
|
||||||
|
titolo: string;
|
||||||
|
/** Frasi di spiegazione, una per paragrafo. */
|
||||||
|
testi?: string[];
|
||||||
|
/** Elenco di persone con il loro dettaglio; con più di una riga compare il conteggio. */
|
||||||
|
elenco?: RigaAvviso[];
|
||||||
|
/** Mostra la freccia: tutta la card porta a un'altra schermata. */
|
||||||
|
cliccabile?: boolean;
|
||||||
|
/** Parola breve accanto al titolo (es. «Oggi»), per dire subito quando riguarda. */
|
||||||
|
etichetta?: string;
|
||||||
|
/** I testi in riquadri propri, uno per messaggio: per chi deve fare una cosa precisa. */
|
||||||
|
riquadri?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Contenuto dell'avviso, senza margini né link: è la parte che si può verificare nei test. */
|
||||||
|
export function CorpoAvviso({
|
||||||
|
tono,
|
||||||
|
icona: Icona,
|
||||||
|
titolo,
|
||||||
|
testi = [],
|
||||||
|
elenco = [],
|
||||||
|
cliccabile = false,
|
||||||
|
etichetta,
|
||||||
|
riquadri = false,
|
||||||
|
}: ProprietaCorpo) {
|
||||||
|
return (
|
||||||
|
<div className={cn("rounded-3xl p-5 shadow-pop", toni[tono])}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="grid h-11 w-11 shrink-0 place-items-center rounded-2xl bg-current/15"
|
||||||
|
>
|
||||||
|
<Icona className="h-6 w-6" />
|
||||||
|
</span>
|
||||||
|
<p className="min-w-0 flex-1 font-display text-lg uppercase leading-tight tracking-wide">
|
||||||
|
{titolo}
|
||||||
|
</p>
|
||||||
|
{etichetta ? (
|
||||||
|
<span className="rounded-full bg-black/25 px-3 py-1 text-[13px] font-bold uppercase tracking-wide">
|
||||||
|
{etichetta}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{elenco.length > 1 ? (
|
||||||
|
<span
|
||||||
|
aria-label={`${elenco.length} persone`}
|
||||||
|
className="rounded-full bg-current/15 px-3 py-1 text-[13px] font-bold tabular-nums"
|
||||||
|
>
|
||||||
|
{elenco.length}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{cliccabile ? <ChevronRight aria-hidden className="h-6 w-6 shrink-0" /> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testi.map((t) => (
|
||||||
|
<p
|
||||||
|
key={t}
|
||||||
|
className={cn(
|
||||||
|
"mt-3 text-[15px] leading-snug",
|
||||||
|
riquadri && "rounded-2xl bg-black/15 p-3.5 font-semibold",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{elenco.length > 0 ? (
|
||||||
|
<ul className="mt-3 divide-y divide-current/15 border-t border-current/15">
|
||||||
|
{elenco.map((r) => (
|
||||||
|
<li
|
||||||
|
key={`${r.chi}-${r.cosa}`}
|
||||||
|
className="flex items-baseline justify-between gap-3 py-2.5 text-[15px] leading-snug"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 truncate font-semibold">{r.chi}</span>
|
||||||
|
<span className="shrink-0 text-right tabular-nums">{r.cosa}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Avviso in Home: comparsa graduale, margini della colonna e, se serve, link ai documenti del
|
||||||
|
* profilo. Tutti gli avvisi (certificati, turno palloni) passano da qui per avere lo stesso aspetto.
|
||||||
|
*/
|
||||||
|
export function Avviso({ indice = 0, ...corpo }: ProprietaCorpo & { indice?: number }) {
|
||||||
|
const card = <CorpoAvviso {...corpo} />;
|
||||||
|
return (
|
||||||
|
<Reveal indice={indice} className="mx-5 mt-4">
|
||||||
|
{corpo.cliccabile ? (
|
||||||
|
<Link
|
||||||
|
to="/profilo"
|
||||||
|
search={{ tab: "documenti" }}
|
||||||
|
className="premi block rounded-3xl focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
||||||
|
>
|
||||||
|
{card}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div role="status">{card}</div>
|
||||||
|
)}
|
||||||
|
</Reveal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertCircle } from "lucide-react";
|
import { CalendarClock, FileClock, FileX } from "lucide-react";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Avviso, type RigaAvviso } from "@/components/crapp/Avviso";
|
||||||
import { useProfili } from "@/lib/profili";
|
import { useProfili } from "@/lib/profili";
|
||||||
import {
|
import {
|
||||||
avvisiCertificatiUtente,
|
avvisiCertificatiUtente,
|
||||||
@@ -12,45 +12,6 @@ import { oggiISO } from "@/lib/palloni-core";
|
|||||||
import { useIsAdmin, useRuoliPronti } from "@/lib/ruoli";
|
import { useIsAdmin, useRuoliPronti } from "@/lib/ruoli";
|
||||||
import { useGiocatoreBase } from "@/lib/user-store";
|
import { useGiocatoreBase } from "@/lib/user-store";
|
||||||
|
|
||||||
function Card({
|
|
||||||
colore,
|
|
||||||
titolo,
|
|
||||||
righe,
|
|
||||||
cliccabile,
|
|
||||||
}: {
|
|
||||||
colore: "warning" | "primary" | "destructive";
|
|
||||||
titolo: string;
|
|
||||||
righe: string[];
|
|
||||||
cliccabile?: boolean;
|
|
||||||
}) {
|
|
||||||
const classi = `mx-5 mt-4 block rounded-3xl p-4 shadow-pop ${
|
|
||||||
colore === "warning"
|
|
||||||
? "bg-warning text-warning-foreground"
|
|
||||||
: colore === "destructive"
|
|
||||||
? "bg-destructive text-destructive-foreground"
|
|
||||||
: "bg-primary text-primary-foreground"
|
|
||||||
} ${cliccabile ? "premi" : ""}`;
|
|
||||||
const contenuto = (
|
|
||||||
<>
|
|
||||||
<p className="flex items-center gap-2 font-display text-lg uppercase leading-none">
|
|
||||||
<AlertCircle className="h-5 w-5" /> {titolo}
|
|
||||||
</p>
|
|
||||||
{righe.map((r) => (
|
|
||||||
<p key={r} className="mt-2 text-xs leading-snug opacity-90">
|
|
||||||
{r}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
return cliccabile ? (
|
|
||||||
<Link to="/profilo" search={{ tab: "documenti" }} className={classi}>
|
|
||||||
{contenuto}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div className={classi}>{contenuto}</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Avviso certificati mancanti/scaduti/in scadenza (DD-035, DD-041, DD-046): calcolato al volo, niente push.
|
* Avviso certificati mancanti/scaduti/in scadenza (DD-035, DD-041, DD-046): calcolato al volo, niente push.
|
||||||
* Il giocatore lo vede da 30 giorni prima della scadenza, lo staff da 7.
|
* Il giocatore lo vede da 30 giorni prima della scadenza, lo staff da 7.
|
||||||
@@ -72,28 +33,31 @@ export function AvvisoCertificati() {
|
|||||||
base,
|
base,
|
||||||
});
|
});
|
||||||
const cardPersonale = personaleMancante ? (
|
const cardPersonale = personaleMancante ? (
|
||||||
<Card
|
<Avviso
|
||||||
colore="destructive"
|
tono="critico"
|
||||||
|
icona={FileX}
|
||||||
titolo="Certificato mancante"
|
titolo="Certificato mancante"
|
||||||
cliccabile
|
cliccabile
|
||||||
righe={["Non hai ancora caricato il certificato medico: caricalo nei documenti"]}
|
testi={["Non hai ancora caricato il certificato medico: caricalo nei documenti"]}
|
||||||
/>
|
/>
|
||||||
) : personale ? (
|
) : personale ? (
|
||||||
personale.giorni < 0 ? (
|
personale.giorni < 0 ? (
|
||||||
<Card
|
<Avviso
|
||||||
colore="primary"
|
tono="scaduto"
|
||||||
|
icona={FileClock}
|
||||||
titolo="Certificato scaduto"
|
titolo="Certificato scaduto"
|
||||||
cliccabile
|
cliccabile
|
||||||
righe={[
|
testi={[
|
||||||
`Il tuo certificato medico è scaduto il ${formatDataBreve(personale.scadenza)}: caricane uno nuovo`,
|
`Il tuo certificato medico è scaduto il ${formatDataBreve(personale.scadenza)}: caricane uno nuovo`,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Card
|
<Avviso
|
||||||
colore="warning"
|
tono="attenzione"
|
||||||
|
icona={CalendarClock}
|
||||||
titolo="Certificato in scadenza"
|
titolo="Certificato in scadenza"
|
||||||
cliccabile
|
cliccabile
|
||||||
righe={[`Il tuo certificato medico ${testoScadenza(personale.giorni)}`]}
|
testi={[`Il tuo certificato medico ${testoScadenza(personale.giorni)}`]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : null;
|
) : null;
|
||||||
@@ -104,30 +68,33 @@ export function AvvisoCertificati() {
|
|||||||
<>
|
<>
|
||||||
{cardPersonale}
|
{cardPersonale}
|
||||||
{mancanti.length > 0 ? (
|
{mancanti.length > 0 ? (
|
||||||
<Card
|
<Avviso
|
||||||
colore="destructive"
|
tono="critico"
|
||||||
|
icona={FileX}
|
||||||
titolo="Certificati mancanti"
|
titolo="Certificati mancanti"
|
||||||
righe={mancanti.map((m) => `${m.nome} ${m.cognome} — non caricato`)}
|
elenco={mancanti.map((m) => ({ chi: `${m.nome} ${m.cognome}`, cosa: "non caricato" }))}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{scaduti.length > 0 ? (
|
{scaduti.length > 0 ? (
|
||||||
<Card
|
<Avviso
|
||||||
colore="primary"
|
tono="scaduto"
|
||||||
|
icona={FileClock}
|
||||||
titolo="Certificati scaduti"
|
titolo="Certificati scaduti"
|
||||||
righe={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))}
|
elenco={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{inScadenza.length > 0 ? (
|
{inScadenza.length > 0 ? (
|
||||||
<Card
|
<Avviso
|
||||||
colore="warning"
|
tono="attenzione"
|
||||||
|
icona={CalendarClock}
|
||||||
titolo="Certificati in scadenza"
|
titolo="Certificati in scadenza"
|
||||||
righe={inScadenza.map((a) => rigaStaff(a, testoScadenza(a.giorni)))}
|
elenco={inScadenza.map((a) => rigaStaff(a, testoScadenza(a.giorni)))}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rigaStaff(a: AvvisoCertificato, dettaglio: string): string {
|
function rigaStaff(a: AvvisoCertificato, dettaglio: string): RigaAvviso {
|
||||||
return `${a.nome} ${a.cognome} — ${dettaglio}`;
|
return { chi: `${a.nome} ${a.cognome}`, cosa: dettaglio };
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
|
|||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<DrawerTitle className="text-xl font-bold leading-tight">{def.nome}</DrawerTitle>
|
<DrawerTitle className="text-xl font-bold leading-tight">{def.nome}</DrawerTitle>
|
||||||
<DrawerDescription className="mt-1 text-sm leading-relaxed">
|
<DrawerDescription className="mt-1 text-[15px] leading-relaxed">
|
||||||
{def.descrizione}
|
{def.descrizione}
|
||||||
</DrawerDescription>
|
</DrawerDescription>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,13 +42,15 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
|
|||||||
{stato ? (
|
{stato ? (
|
||||||
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
|
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Stato attuale
|
Stato attuale
|
||||||
</p>
|
</p>
|
||||||
{grado ? (
|
{grado ? (
|
||||||
<span className={cn("text-xs font-bold uppercase", meta?.text)}>{meta?.label}</span>
|
<span className={cn("text-[13px] font-bold uppercase", meta?.text)}>
|
||||||
|
{meta?.label}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs font-bold uppercase text-muted-foreground">
|
<span className="text-[13px] font-bold uppercase text-muted-foreground">
|
||||||
In progresso
|
In progresso
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -59,19 +61,19 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
|
|||||||
{stato.prossimaSoglia ? (
|
{stato.prossimaSoglia ? (
|
||||||
<>
|
<>
|
||||||
<Barra percentuale={stato.progresso} trackClassName="mt-3" />
|
<Barra percentuale={stato.progresso} trackClassName="mt-3" />
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
Mancano {stato.prossimaSoglia - stato.valore} {def.unita} per il{" "}
|
Mancano {stato.prossimaSoglia - stato.valore} {def.unita} per il{" "}
|
||||||
{gradoMeta[stato.prossimo as Grado].label.toLowerCase()}
|
{gradoMeta[stato.prossimo as Grado].label.toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-2 text-xs text-success">Hai raggiunto il livello massimo.</p>
|
<p className="mt-2 text-[13px] text-success">Hai raggiunto il livello massimo.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Soglie
|
Soglie
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
@@ -88,7 +90,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
|
|||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs font-bold uppercase",
|
"text-[13px] font-bold uppercase",
|
||||||
raggiunto ? gm.text : "text-muted-foreground",
|
raggiunto ? gm.text : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -102,7 +104,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
|
|||||||
>
|
>
|
||||||
{def.soglie[g]}
|
{def.soglie[g]}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">{def.unita}</p>
|
<p className="text-[13px] text-muted-foreground">{def.unita}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -110,7 +112,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{def.celebrazione ? (
|
{def.celebrazione ? (
|
||||||
<p className="rounded-2xl bg-accent/10 p-3 text-center text-sm font-semibold text-accent">
|
<p className="rounded-2xl bg-accent/10 p-3 text-center text-[15px] font-semibold text-accent">
|
||||||
“{def.celebrazione}”
|
“{def.celebrazione}”
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -142,7 +144,7 @@ export function BadgeDrawer({
|
|||||||
<DrawerClose asChild>
|
<DrawerClose asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-2xl bg-secondary py-3 text-sm font-semibold text-foreground transition-colors hover:bg-secondary/80"
|
className="w-full rounded-2xl bg-secondary py-3 text-[15px] font-semibold text-foreground transition-colors hover:bg-secondary/80"
|
||||||
>
|
>
|
||||||
Indietro
|
Indietro
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ export function CelebrazioneBadge() {
|
|||||||
{notifica.emoji}
|
{notifica.emoji}
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-accent">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-accent">
|
||||||
{notifica.tono === "segreto" ? "Badge segreto sbloccato" : "Traguardo raggiunto"}
|
{notifica.tono === "segreto" ? "Badge segreto sbloccato" : "Traguardo raggiunto"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-0.5 font-display text-2xl leading-none">{notifica.titolo}</p>
|
<p className="mt-0.5 font-display text-2xl leading-none">{notifica.titolo}</p>
|
||||||
<p className="mt-1.5 text-sm text-muted-foreground">{notifica.testo}</p>
|
<p className="mt-1.5 text-[15px] text-muted-foreground">{notifica.testo}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -69,7 +69,7 @@ export function CelebrazioneBadge() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={chiudi}
|
onClick={chiudi}
|
||||||
className="mt-4 w-full rounded-2xl bg-accent-grad px-4 py-3 text-sm font-bold uppercase tracking-wide text-accent-foreground"
|
className="mt-4 w-full rounded-2xl bg-accent-grad px-4 py-3 text-[15px] font-bold uppercase tracking-wide text-accent-foreground"
|
||||||
>
|
>
|
||||||
{restanti > 0 ? `Avanti (${restanti})` : "Grande!"}
|
{restanti > 0 ? `Avanti (${restanti})` : "Grande!"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -45,21 +45,21 @@ function CardBadge({ b, opaco, indice = 0 }: { b: BadgeStato; opaco?: boolean; i
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Icon className={cn("h-6 w-6", meta ? meta.text : "text-muted-foreground/50")} />
|
<Icon className={cn("h-6 w-6", meta ? meta.text : "text-muted-foreground/50")} />
|
||||||
{meta ? (
|
{meta ? (
|
||||||
<span className={cn("text-xs font-bold uppercase", meta.text)}>{meta.label}</span>
|
<span className={cn("text-[13px] font-bold uppercase", meta.text)}>{meta.label}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1.5 text-sm font-bold leading-tight">{b.def.nome}</p>
|
<p className="mt-1.5 text-[15px] font-bold leading-tight">{b.def.nome}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{b.valore} {b.def.unita}
|
{b.valore} {b.def.unita}
|
||||||
</p>
|
</p>
|
||||||
{b.prossimaSoglia ? (
|
{b.prossimaSoglia ? (
|
||||||
<>
|
<>
|
||||||
<Barra percentuale={b.progresso} altezza="h-1.5" trackClassName="mt-2" />
|
<Barra percentuale={b.progresso} altezza="h-1.5" trackClassName="mt-2" />
|
||||||
<p className="mt-1 text-xs font-semibold text-accent">{mancanoPer(b)}</p>
|
<p className="mt-1 text-[13px] font-semibold text-accent">{mancanoPer(b)}</p>
|
||||||
<p className="text-xs text-muted-foreground">{microcopyBadge(b)}</p>
|
<p className="text-[13px] text-muted-foreground">{microcopyBadge(b)}</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-2 text-xs text-muted-foreground">{microcopyBadge(b)}</p>
|
<p className="mt-2 text-[13px] text-muted-foreground">{microcopyBadge(b)}</p>
|
||||||
)}
|
)}
|
||||||
</Reveal>
|
</Reveal>
|
||||||
);
|
);
|
||||||
@@ -90,13 +90,15 @@ function SocialDrawer({
|
|||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<DrawerTitle className="text-xl font-bold leading-tight">{cat.nome}</DrawerTitle>
|
<DrawerTitle className="text-xl font-bold leading-tight">{cat.nome}</DrawerTitle>
|
||||||
<DrawerDescription className="mt-1 text-sm leading-relaxed">
|
<DrawerDescription className="mt-1 text-[15px] leading-relaxed">
|
||||||
{cat.descrizione}
|
{cat.descrizione}
|
||||||
</DrawerDescription>
|
</DrawerDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
|
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">Vinto</p>
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Vinto
|
||||||
|
</p>
|
||||||
<p className="mt-1 font-display text-3xl leading-none">
|
<p className="mt-1 font-display text-3xl leading-none">
|
||||||
{conteggio}{" "}
|
{conteggio}{" "}
|
||||||
<span className="text-lg text-muted-foreground">
|
<span className="text-lg text-muted-foreground">
|
||||||
@@ -109,7 +111,7 @@ function SocialDrawer({
|
|||||||
<DrawerClose asChild>
|
<DrawerClose asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-2xl bg-secondary py-3 text-sm font-semibold text-foreground transition-colors hover:bg-secondary/80"
|
className="w-full rounded-2xl bg-secondary py-3 text-[15px] font-semibold text-foreground transition-colors hover:bg-secondary/80"
|
||||||
>
|
>
|
||||||
Indietro
|
Indietro
|
||||||
</button>
|
</button>
|
||||||
@@ -139,7 +141,7 @@ export function CollezioneBadge({
|
|||||||
<div className="rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
|
<div className="rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
|
||||||
<div className="flex items-end justify-between gap-3">
|
<div className="flex items-end justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-primary-foreground/60">
|
<p className="text-[13px] font-semibold uppercase tracking-wide text-primary-foreground/60">
|
||||||
Collezione badge
|
Collezione badge
|
||||||
</p>
|
</p>
|
||||||
<p className="font-display text-4xl leading-none">
|
<p className="font-display text-4xl leading-none">
|
||||||
@@ -147,7 +149,7 @@ export function CollezioneBadge({
|
|||||||
<span className="text-2xl text-primary-foreground/50">/{c.totali}</span>
|
<span className="text-2xl text-primary-foreground/50">/{c.totali}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-primary-foreground/70">
|
<p className="text-[13px] text-primary-foreground/70">
|
||||||
<Numero valore={pct} suffisso="%" /> completata
|
<Numero valore={pct} suffisso="%" /> completata
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,12 +158,12 @@ export function CollezioneBadge({
|
|||||||
|
|
||||||
{vicino ? (
|
{vicino ? (
|
||||||
<div className="rounded-3xl bg-card p-4 shadow-card ring-1 ring-accent/30">
|
<div className="rounded-3xl bg-card p-4 shadow-card ring-1 ring-accent/30">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-accent">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-accent">
|
||||||
Prossimo traguardo
|
Prossimo traguardo
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-sm font-bold leading-tight">{vicino.def.nome}</p>
|
<p className="mt-1 text-[15px] font-bold leading-tight">{vicino.def.nome}</p>
|
||||||
<Barra percentuale={vicino.progresso} trackClassName="mt-2" />
|
<Barra percentuale={vicino.progresso} trackClassName="mt-2" />
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
{mancanoPer(vicino)} · {microcopyBadge(vicino)}
|
{mancanoPer(vicino)} · {microcopyBadge(vicino)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +171,7 @@ export function CollezioneBadge({
|
|||||||
|
|
||||||
{c.sbloccati.length > 0 ? (
|
{c.sbloccati.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Sbloccati ({c.sbloccati.length})
|
Sbloccati ({c.sbloccati.length})
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -184,7 +186,7 @@ export function CollezioneBadge({
|
|||||||
|
|
||||||
{c.inProgresso.length > 0 ? (
|
{c.inProgresso.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
In progresso ({c.inProgresso.length})
|
In progresso ({c.inProgresso.length})
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -199,7 +201,7 @@ export function CollezioneBadge({
|
|||||||
|
|
||||||
{socialVinti.length > 0 ? (
|
{socialVinti.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Votati dai compagni
|
Votati dai compagni
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -207,8 +209,8 @@ export function CollezioneBadge({
|
|||||||
<SocialDrawer key={cat.id} cat={cat} conteggio={social[cat.id] ?? 0}>
|
<SocialDrawer key={cat.id} cat={cat} conteggio={social[cat.id] ?? 0}>
|
||||||
<div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-accent/25">
|
<div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-accent/25">
|
||||||
<p className="text-lg leading-none">{cat.emoji}</p>
|
<p className="text-lg leading-none">{cat.emoji}</p>
|
||||||
<p className="mt-1 text-sm font-bold leading-tight">{cat.nome}</p>
|
<p className="mt-1 text-[15px] font-bold leading-tight">{cat.nome}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
Vinto {social[cat.id]} {social[cat.id] === 1 ? "volta" : "volte"}
|
Vinto {social[cat.id]} {social[cat.id] === 1 ? "volta" : "volte"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +221,7 @@ export function CollezioneBadge({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Badge segreti
|
Badge segreti
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -229,8 +231,8 @@ export function CollezioneBadge({
|
|||||||
<BadgeDrawer key={b.def.id} def={b.def} stato={b}>
|
<BadgeDrawer key={b.def.id} def={b.def} stato={b}>
|
||||||
<div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-oro/40">
|
<div className="rounded-2xl bg-card p-3 shadow-card ring-1 ring-oro/40">
|
||||||
<Icon className="h-6 w-6 text-oro" />
|
<Icon className="h-6 w-6 text-oro" />
|
||||||
<p className="mt-1.5 text-sm font-bold leading-tight">{b.def.nome}</p>
|
<p className="mt-1.5 text-[15px] font-bold leading-tight">{b.def.nome}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{b.def.celebrazione ?? "Badge segreto sbloccato."}
|
{b.def.celebrazione ?? "Badge segreto sbloccato."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,8 +245,10 @@ export function CollezioneBadge({
|
|||||||
className="grid place-items-center rounded-2xl bg-secondary/60 p-3 text-center"
|
className="grid place-items-center rounded-2xl bg-secondary/60 p-3 text-center"
|
||||||
>
|
>
|
||||||
<Lucchetto className="h-6 w-6 text-muted-foreground/50" />
|
<Lucchetto className="h-6 w-6 text-muted-foreground/50" />
|
||||||
<p className="mt-1.5 text-sm font-bold leading-tight text-muted-foreground">???</p>
|
<p className="mt-1.5 text-[15px] font-bold leading-tight text-muted-foreground">
|
||||||
<p className="text-xs text-muted-foreground/70">Badge segreto da scoprire</p>
|
???
|
||||||
|
</p>
|
||||||
|
<p className="text-[13px] text-muted-foreground/70">Badge segreto da scoprire</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function MetaPartitaCsi({
|
|||||||
.join(" · ");
|
.join(" · ");
|
||||||
if (!voci && !link) return null;
|
if (!voci && !link) return null;
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<div className="mt-3 flex flex-wrap items-center gap-2 text-[13px] text-muted-foreground">
|
||||||
{voci ? <span>{voci}</span> : null}
|
{voci ? <span>{voci}</span> : null}
|
||||||
{link ? (
|
{link ? (
|
||||||
<a
|
<a
|
||||||
@@ -68,17 +68,17 @@ export function MetaPartitaCsi({
|
|||||||
function ListaFormazione({ squadra }: { squadra: FormazioneSquadra }) {
|
function ListaFormazione({ squadra }: { squadra: FormazioneSquadra }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
{squadra.squadra}
|
{squadra.squadra}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{squadra.titolari.map((g, i) => (
|
{squadra.titolari.map((g, i) => (
|
||||||
<div key={i} className="flex items-center gap-2 text-sm">
|
<div key={i} className="flex items-center gap-2 text-[15px]">
|
||||||
<span className="w-6 shrink-0 text-center font-display text-xs text-accent">
|
<span className="w-6 shrink-0 text-center font-display text-[13px] text-accent">
|
||||||
{g.numero}
|
{g.numero}
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1 truncate">{g.nome}</span>
|
<span className="min-w-0 flex-1 truncate">{g.nome}</span>
|
||||||
{g.ruolo ? <span className="text-xs text-muted-foreground">{g.ruolo}</span> : null}
|
{g.ruolo ? <span className="text-[13px] text-muted-foreground">{g.ruolo}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +89,7 @@ function ListaFormazione({ squadra }: { squadra: FormazioneSquadra }) {
|
|||||||
</p>
|
</p>
|
||||||
<div className="mt-1 space-y-1">
|
<div className="mt-1 space-y-1">
|
||||||
{squadra.panchina.map((g, i) => (
|
{squadra.panchina.map((g, i) => (
|
||||||
<div key={i} className="flex items-center gap-2 text-xs text-muted-foreground">
|
<div key={i} className="flex items-center gap-2 text-[13px] text-muted-foreground">
|
||||||
<span className="w-6 shrink-0 text-center">{g.numero}</span>
|
<span className="w-6 shrink-0 text-center">{g.numero}</span>
|
||||||
<span className="min-w-0 flex-1 truncate">{g.nome}</span>
|
<span className="min-w-0 flex-1 truncate">{g.nome}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +98,7 @@ function ListaFormazione({ squadra }: { squadra: FormazioneSquadra }) {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{squadra.staff.length > 0 ? (
|
{squadra.staff.length > 0 ? (
|
||||||
<div className="mt-3 space-y-0.5 text-xs text-muted-foreground">
|
<div className="mt-3 space-y-0.5 text-[13px] text-muted-foreground">
|
||||||
{squadra.staff.map((s, i) => (
|
{squadra.staff.map((s, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
{s.nome} · {s.ruolo}
|
{s.nome} · {s.ruolo}
|
||||||
@@ -121,7 +121,7 @@ function BarraPrecedenti({
|
|||||||
}) {
|
}) {
|
||||||
const totale = noi + avversario || 1;
|
const totale = noi + avversario || 1;
|
||||||
return (
|
return (
|
||||||
<div className="text-xs">
|
<div className="text-[13px]">
|
||||||
<div className="flex items-center justify-between text-muted-foreground">
|
<div className="flex items-center justify-between text-muted-foreground">
|
||||||
<span className="font-bold text-foreground">{noi}</span>
|
<span className="font-bold text-foreground">{noi}</span>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
@@ -139,7 +139,7 @@ const NOME_NOI = "CRAP Volley";
|
|||||||
/** Intestazione con i nomi delle due squadre, per non dover ripeterli su ogni barra sotto. */
|
/** Intestazione con i nomi delle due squadre, per non dover ripeterli su ogni barra sotto. */
|
||||||
function TestataSquadre({ avversario }: { avversario: string }) {
|
function TestataSquadre({ avversario }: { avversario: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between text-xs font-bold">
|
<div className="flex items-center justify-between text-[13px] font-bold">
|
||||||
<span className="truncate text-accent">{NOME_NOI}</span>
|
<span className="truncate text-accent">{NOME_NOI}</span>
|
||||||
<span className="truncate text-right text-muted-foreground">{avversario}</span>
|
<span className="truncate text-right text-muted-foreground">{avversario}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,7 +158,7 @@ function BarraProbabilita({
|
|||||||
const favoritaNoi = noi >= avversarioPct;
|
const favoritaNoi = noi >= avversarioPct;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs text-muted-foreground">
|
<p className="mb-2 text-[13px] text-muted-foreground">
|
||||||
Probabilità di vittoria (calcolo CSI):{" "}
|
Probabilità di vittoria (calcolo CSI):{" "}
|
||||||
<span className="font-bold text-foreground">{favoritaNoi ? NOME_NOI : avversario}</span>{" "}
|
<span className="font-bold text-foreground">{favoritaNoi ? NOME_NOI : avversario}</span>{" "}
|
||||||
favorita al {Math.max(noi, avversarioPct).toFixed(0)}%.
|
favorita al {Math.max(noi, avversarioPct).toFixed(0)}%.
|
||||||
@@ -171,13 +171,13 @@ function BarraProbabilita({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex h-7 overflow-hidden rounded-full">
|
<div className="flex h-7 overflow-hidden rounded-full">
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center bg-success text-xs font-bold text-success-foreground"
|
className="flex items-center justify-center bg-success text-[13px] font-bold text-success-foreground"
|
||||||
style={{ width: `${noi}%` }}
|
style={{ width: `${noi}%` }}
|
||||||
>
|
>
|
||||||
{noi.toFixed(0)}%
|
{noi.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center bg-destructive text-xs font-bold text-destructive-foreground"
|
className="flex items-center justify-center bg-destructive text-[13px] font-bold text-destructive-foreground"
|
||||||
style={{ width: `${avversarioPct}%` }}
|
style={{ width: `${avversarioPct}%` }}
|
||||||
>
|
>
|
||||||
{avversarioPct.toFixed(0)}%
|
{avversarioPct.toFixed(0)}%
|
||||||
@@ -197,7 +197,7 @@ function BloccoPrecedenti({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<TestataSquadre avversario={avversario} />
|
<TestataSquadre avversario={avversario} />
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{precedenti.totale > 0
|
{precedenti.totale > 0
|
||||||
? `${precedenti.totale} precedenti in archivio sul portale CSI.`
|
? `${precedenti.totale} precedenti in archivio sul portale CSI.`
|
||||||
: "Nessun precedente in archivio sul portale CSI: primo confronto tra le due squadre."}
|
: "Nessun precedente in archivio sul portale CSI: primo confronto tra le due squadre."}
|
||||||
@@ -248,20 +248,22 @@ export function DettaglioCsiEsteso({
|
|||||||
const { data, isLoading } = useCsiPartita(matchId);
|
const { data, isLoading } = useCsiPartita(matchId);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <p className="px-1 text-center text-xs text-muted-foreground">Carico i dati dal CSI…</p>;
|
return (
|
||||||
|
<p className="px-1 text-center text-[13px] text-muted-foreground">Carico i dati dal CSI…</p>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!data || (!data.formazioni && !data.precedenti)) return null;
|
if (!data || (!data.formazioni && !data.precedenti)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{data.giornata || data.nota ? (
|
{data.giornata || data.nota ? (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{[data.giornata, data.nota].filter(Boolean).join(" · ")}
|
{[data.giornata, data.nota].filter(Boolean).join(" · ")}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{data.formazioni ? (
|
{data.formazioni ? (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="mb-3 flex items-center gap-2 text-sm font-bold">
|
<div className="mb-3 flex items-center gap-2 text-[15px] font-bold">
|
||||||
<Users2 className="h-4 w-4 text-accent" /> Formazioni
|
<Users2 className="h-4 w-4 text-accent" /> Formazioni
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
@@ -272,7 +274,7 @@ export function DettaglioCsiEsteso({
|
|||||||
) : null}
|
) : null}
|
||||||
{data.precedenti ? (
|
{data.precedenti ? (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="mb-3 flex items-center gap-2 text-sm font-bold">
|
<div className="mb-3 flex items-center gap-2 text-[15px] font-bold">
|
||||||
<ShieldAlert className="h-4 w-4 text-accent" /> Scontri diretti
|
<ShieldAlert className="h-4 w-4 text-accent" /> Scontri diretti
|
||||||
</div>
|
</div>
|
||||||
<BloccoPrecedenti precedenti={data.precedenti} avversario={avversario} />
|
<BloccoPrecedenti precedenti={data.precedenti} avversario={avversario} />
|
||||||
|
|||||||
@@ -101,10 +101,10 @@ export function EventoCard({
|
|||||||
<Cake className="h-5 w-5" />
|
<Cake className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h3 className="truncate text-sm font-bold leading-tight">{evento.titolo}</h3>
|
<h3 className="truncate text-[15px] font-bold leading-tight">{evento.titolo}</h3>
|
||||||
<p className="text-xs text-muted-foreground">{evento.luogo}</p>
|
<p className="text-[13px] text-muted-foreground">{evento.luogo}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 text-right text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<div className="shrink-0 text-right text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
{etichettaData(evento.data)}
|
{etichettaData(evento.data)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -139,13 +139,13 @@ export function EventoCard({
|
|||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-block rounded-full px-2.5 py-0.5 text-xs font-bold uppercase tracking-wide",
|
"inline-block rounded-full px-2.5 py-0.5 text-[13px] font-bold uppercase tracking-wide",
|
||||||
tipo.className,
|
tipo.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tipo.label}
|
{tipo.label}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex shrink-0 items-center gap-0.5 text-sm font-bold uppercase tracking-wide text-muted-foreground">
|
<span className="inline-flex shrink-0 items-center gap-0.5 text-[15px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
{etichettaData(evento.data)}
|
{etichettaData(evento.data)}
|
||||||
{linkTo ? <ChevronRight className="h-4 w-4" aria-hidden /> : null}
|
{linkTo ? <ChevronRight className="h-4 w-4" aria-hidden /> : null}
|
||||||
</span>
|
</span>
|
||||||
@@ -153,10 +153,10 @@ export function EventoCard({
|
|||||||
|
|
||||||
<h3 className="mt-2 text-base font-bold leading-tight">{evento.titolo}</h3>
|
<h3 className="mt-2 text-base font-bold leading-tight">{evento.titolo}</h3>
|
||||||
{sottotitoloPartita ? (
|
{sottotitoloPartita ? (
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">{sottotitoloPartita}</p>
|
<p className="mt-0.5 text-[13px] text-muted-foreground">{sottotitoloPartita}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-2.5 flex flex-wrap gap-x-3 gap-y-1 text-[13px] text-muted-foreground">
|
<div className="mt-2.5 flex flex-wrap gap-x-3 gap-y-1 text-[14px] text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<Clock className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
<Clock className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
||||||
<span>{evento.ora}</span>
|
<span>{evento.ora}</span>
|
||||||
@@ -174,12 +174,12 @@ export function EventoCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{noteCard ? (
|
{noteCard ? (
|
||||||
<p className="mt-2.5 line-clamp-2 text-xs text-muted-foreground">📝 {noteCard}</p>
|
<p className="mt-2.5 line-clamp-2 text-[13px] text-muted-foreground">📝 {noteCard}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{metaStato ? (
|
{metaStato ? (
|
||||||
<p
|
<p
|
||||||
className="mt-3 flex items-center gap-1.5 border-t border-border pt-2.5 text-left text-xs font-semibold text-muted-foreground"
|
className="mt-3 flex items-center gap-1.5 border-t border-border pt-2.5 text-left text-[13px] font-semibold text-muted-foreground"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<span aria-hidden>{metaStato.emoji}</span>
|
<span aria-hidden>{metaStato.emoji}</span>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useState, type ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { formatData } from "@/lib/crapp-data";
|
||||||
|
import { microcopyObiettivo, progressoObiettivo, type ObiettivoSquadra } from "@/lib/obiettivi";
|
||||||
|
import { Barra } from "@/components/motion/Barra";
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerDescription,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from "@/components/ui/drawer";
|
||||||
|
|
||||||
|
function Elenco({ titolo, voci, segno }: { titolo: string; voci: string[]; segno: string }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
|
{titolo}
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{voci.map((v) => (
|
||||||
|
<li key={v} className="flex gap-2 text-[15px] leading-snug">
|
||||||
|
<span aria-hidden className="shrink-0 font-bold text-accent">
|
||||||
|
{segno}
|
||||||
|
</span>
|
||||||
|
<span>{v}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Blocco({ titolo, testo }: { titolo: string; testo: string }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="mb-1 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
|
{titolo}
|
||||||
|
</p>
|
||||||
|
<p className="text-[15px] leading-relaxed">{testo}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Corpo della card di dettaglio; esportato per i test, va usato dentro un Drawer. */
|
||||||
|
export function SchedaObiettivo({ o }: { o: ObiettivoSquadra }) {
|
||||||
|
const pct = progressoObiettivo(o);
|
||||||
|
const fatto = pct >= 100;
|
||||||
|
const d = o.dettaglio;
|
||||||
|
return (
|
||||||
|
<div className="max-h-[70vh] space-y-4 overflow-y-auto p-4 pt-0">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="grid h-14 w-14 shrink-0 place-items-center rounded-2xl bg-secondary text-2xl ring-1 ring-border">
|
||||||
|
{o.emoji}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<DrawerTitle className="text-xl font-bold leading-tight">{o.titolo}</DrawerTitle>
|
||||||
|
<DrawerDescription className="mt-1 text-[15px] leading-relaxed">
|
||||||
|
{o.descrizione}
|
||||||
|
</DrawerDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl bg-card p-4 shadow-card ring-1 ring-border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Stato attuale
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-[13px] font-bold uppercase",
|
||||||
|
fatto ? "text-success" : "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{fatto ? "Completato" : "In corso"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 font-display text-3xl leading-none">
|
||||||
|
{o.valore} <span className="text-lg text-muted-foreground">su {o.target}</span>{" "}
|
||||||
|
<span className="text-lg text-muted-foreground">{o.unita}</span>
|
||||||
|
</p>
|
||||||
|
<Barra percentuale={pct} trackClassName="mt-3" />
|
||||||
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
|
{pct}%{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[13px] font-semibold text-accent">{microcopyObiettivo(o)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Blocco titolo="Come si calcola" testo={d.comeSiCalcola} />
|
||||||
|
<Elenco titolo="Cosa conta" voci={d.conta} segno="+" />
|
||||||
|
{d.perse ? <Elenco titolo="Presenza persa" voci={d.perse} segno="−" /> : null}
|
||||||
|
<Elenco
|
||||||
|
titolo={d.perse ? "Non entra nel calcolo" : "Cosa non conta"}
|
||||||
|
voci={d.nonConta}
|
||||||
|
segno={d.perse ? "·" : "−"}
|
||||||
|
/>
|
||||||
|
<Blocco titolo="Quando vale" testo={d.periodo} />
|
||||||
|
<Blocco titolo="Da dove arrivano i dati" testo={d.fonte} />
|
||||||
|
<Blocco titolo="Un esempio" testo={d.esempio} />
|
||||||
|
|
||||||
|
<p className="rounded-2xl bg-accent/10 p-3 text-center text-[15px] font-semibold text-accent">
|
||||||
|
{o.impatto}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drawer cliccabile che spiega nel dettaglio un obiettivo di squadra. */
|
||||||
|
export function ObiettivoDrawer({
|
||||||
|
obiettivo,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
obiettivo: ObiettivoSquadra;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerTrigger asChild>{children}</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<DrawerHeader className="sr-only">
|
||||||
|
<DrawerTitle>{obiettivo.titolo}</DrawerTitle>
|
||||||
|
<DrawerDescription>{obiettivo.descrizione}</DrawerDescription>
|
||||||
|
</DrawerHeader>
|
||||||
|
<SchedaObiettivo o={obiettivo} />
|
||||||
|
<DrawerFooter>
|
||||||
|
<DrawerClose asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full rounded-2xl bg-secondary py-3 text-[15px] font-semibold text-foreground transition-colors hover:bg-secondary/80"
|
||||||
|
>
|
||||||
|
Indietro
|
||||||
|
</button>
|
||||||
|
</DrawerClose>
|
||||||
|
</DrawerFooter>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,19 +45,19 @@ export function Pagelle({
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="inline-flex items-center gap-1.5 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
<ClipboardCheck className="h-3.5 w-3.5" /> Pagelle anonime
|
<ClipboardCheck className="h-3.5 w-3.5" /> Pagelle anonime
|
||||||
</p>
|
</p>
|
||||||
<span className="rounded-full bg-secondary px-2.5 py-1 text-xs font-bold uppercase text-muted-foreground">
|
<span className="rounded-full bg-secondary px-2.5 py-1 text-[13px] font-bold uppercase text-muted-foreground">
|
||||||
{chiuse ? "Votazioni chiuse" : `${fatti}/${daVotare.length} votati`}
|
{chiuse ? "Votazioni chiuse" : `${fatti}/${daVotare.length} votati`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-[13px] text-muted-foreground">
|
||||||
Dai un voto da 1 a 10 ai compagni: nessuno vedrà chi ha votato cosa, solo la media.
|
Dai un voto da 1 a 10 ai compagni: nessuno vedrà chi ha votato cosa, solo la media.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<p aria-busy="true" className="mt-3 text-xs text-muted-foreground">
|
<p aria-busy="true" className="mt-3 text-[13px] text-muted-foreground">
|
||||||
Carico le pagelle…
|
Carico le pagelle…
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -70,15 +70,17 @@ export function Pagelle({
|
|||||||
return (
|
return (
|
||||||
<div key={g.id} className="rounded-2xl bg-secondary/60 p-2.5">
|
<div key={g.id} className="rounded-2xl bg-secondary/60 p-2.5">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Avatar id={g.id} fallback={String(g.numero)} className="h-8 w-8 text-xs" />
|
<Avatar id={g.id} fallback={String(g.numero)} className="h-8 w-8 text-[13px]" />
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
|
<span className="block truncate text-[15px] font-bold leading-tight">
|
||||||
<span className="text-xs text-muted-foreground">
|
{g.nome}
|
||||||
|
</span>
|
||||||
|
<span className="text-[13px] text-muted-foreground">
|
||||||
{media ? `Media ${media.media} · ${media.voti} voti` : "Nessun voto"}
|
{media ? `Media ${media.media} · ${media.voti} voti` : "Nessun voto"}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{sonoIo ? (
|
{sonoIo ? (
|
||||||
<span className="shrink-0 text-xs font-semibold uppercase text-muted-foreground">
|
<span className="shrink-0 text-[13px] font-semibold uppercase text-muted-foreground">
|
||||||
Sei tu
|
Sei tu
|
||||||
</span>
|
</span>
|
||||||
) : chiuse ? (
|
) : chiuse ? (
|
||||||
@@ -89,7 +91,7 @@ export function Pagelle({
|
|||||||
disabled={!io || vota.isPending}
|
disabled={!io || vota.isPending}
|
||||||
onClick={() => setApertoPer(aperto ? null : g.id)}
|
onClick={() => setApertoPer(aperto ? null : g.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 rounded-full px-3 py-1 text-xs font-bold uppercase transition-colors disabled:opacity-50",
|
"shrink-0 rounded-full px-3 py-1 text-[13px] font-bold uppercase transition-colors disabled:opacity-50",
|
||||||
mio !== undefined
|
mio !== undefined
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "bg-card text-foreground",
|
: "bg-card text-foreground",
|
||||||
@@ -107,7 +109,7 @@ export function Pagelle({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => invia(g.id, v)}
|
onClick={() => invia(g.id, v)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg py-1.5 text-xs font-bold tabular-nums transition-transform active:scale-90",
|
"rounded-lg py-1.5 text-[13px] font-bold tabular-nums transition-transform active:scale-90",
|
||||||
mio === v
|
mio === v
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "bg-card text-foreground",
|
: "bg-card text-foreground",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const TIPI_DOCUMENTO = ["Carta d'identità", "Patente", "Passaporto"];
|
|||||||
function Intestazione({ titolo, completa }: { titolo: string; completa: boolean }) {
|
function Intestazione({ titolo, completa }: { titolo: string; completa: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 pt-2">
|
<div className="flex items-center gap-2 pt-2">
|
||||||
<h3 className="font-display text-sm uppercase tracking-wide">{titolo}</h3>
|
<h3 className="font-display text-[15px] uppercase tracking-wide">{titolo}</h3>
|
||||||
{completa ? <Check className="h-4 w-4 text-success" /> : null}
|
{completa ? <Check className="h-4 w-4 text-success" /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -67,7 +67,7 @@ function CampoFile({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-3 py-2">
|
<div className="flex items-center justify-between gap-3 py-2">
|
||||||
<span className="flex min-w-0 items-center gap-2 text-sm">
|
<span className="flex min-w-0 items-center gap-2 text-[15px]">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid h-6 w-6 shrink-0 place-items-center rounded-lg",
|
"grid h-6 w-6 shrink-0 place-items-center rounded-lg",
|
||||||
@@ -93,7 +93,7 @@ function CampoFile({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => input.current?.click()}
|
onClick={() => input.current?.click()}
|
||||||
disabled={inCorso}
|
disabled={inCorso}
|
||||||
className="premi rounded-xl bg-primary px-3 py-2 text-xs font-bold text-primary-foreground disabled:opacity-60"
|
className="premi rounded-xl bg-primary px-3 py-2 text-[13px] font-bold text-primary-foreground disabled:opacity-60"
|
||||||
>
|
>
|
||||||
{inCorso ? <Loader2 className="h-4 w-4 animate-spin" /> : path ? "Sostituisci" : "Carica"}
|
{inCorso ? <Loader2 className="h-4 w-4 animate-spin" /> : path ? "Sostituisci" : "Carica"}
|
||||||
</button>
|
</button>
|
||||||
@@ -336,7 +336,7 @@ export function ProfiloAllenatore({ g }: { g: GiocatoreSquadra }) {
|
|||||||
style={{ width: `${perc}%` }}
|
style={{ width: `${perc}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 text-xs font-bold tabular-nums text-muted-foreground">
|
<span className="shrink-0 text-[13px] font-bold tabular-nums text-muted-foreground">
|
||||||
{perc}%
|
{perc}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -366,7 +366,7 @@ export function ProfiloAllenatore({ g }: { g: GiocatoreSquadra }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void salva()}
|
onClick={() => void salva()}
|
||||||
disabled={!sporco || inCorso}
|
disabled={!sporco || inCorso}
|
||||||
className="premi flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
className="premi flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{inCorso ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
{inCorso ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||||
{sporco ? "Salva" : "Salvato"}
|
{sporco ? "Salva" : "Salvato"}
|
||||||
@@ -430,7 +430,7 @@ export function ProfiloAmministrativo({
|
|||||||
style={{ width: `${perc}%` }}
|
style={{ width: `${perc}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 text-xs font-bold tabular-nums text-muted-foreground">
|
<span className="shrink-0 text-[13px] font-bold tabular-nums text-muted-foreground">
|
||||||
{perc}%
|
{perc}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -472,7 +472,7 @@ export function ProfiloAmministrativo({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={salvaBozza}
|
onClick={salvaBozza}
|
||||||
disabled={!sporco || salva.isPending}
|
disabled={!sporco || salva.isPending}
|
||||||
className="premi flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
className="premi flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||||
{sporco ? "Salva" : "Salvato"}
|
{sporco ? "Salva" : "Salvato"}
|
||||||
@@ -486,7 +486,9 @@ export function ProfiloAmministrativo({
|
|||||||
<SezioneTendina
|
<SezioneTendina
|
||||||
titolo="Dati per il tesseramento"
|
titolo="Dati per il tesseramento"
|
||||||
indice={indice}
|
indice={indice}
|
||||||
azione={<span className="text-xs font-bold tabular-nums text-muted-foreground">{perc}%</span>}
|
azione={
|
||||||
|
<span className="text-[13px] font-bold tabular-nums text-muted-foreground">{perc}%</span>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{corpo}
|
{corpo}
|
||||||
</SezioneTendina>
|
</SezioneTendina>
|
||||||
@@ -521,10 +523,10 @@ export function CompletaProfilo({
|
|||||||
className="premi block rounded-3xl bg-card p-4 shadow-card"
|
className="premi block rounded-3xl bg-card p-4 shadow-card"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="font-display text-[15px] uppercase tracking-wide">
|
<span className="font-display text-[16px] uppercase tracking-wide">
|
||||||
Completa il tuo profilo
|
Completa il tuo profilo
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[13px] font-bold tabular-nums text-muted-foreground">{perc}%</span>
|
<span className="text-[14px] font-bold tabular-nums text-muted-foreground">{perc}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-secondary">
|
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-secondary">
|
||||||
<div
|
<div
|
||||||
@@ -532,7 +534,7 @@ export function CompletaProfilo({
|
|||||||
style={{ width: `${perc}%` }}
|
style={{ width: `${perc}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-[13px] text-muted-foreground">
|
<p className="mt-2 text-[14px] text-muted-foreground">
|
||||||
{allenatore
|
{allenatore
|
||||||
? "Data e luogo di nascita, telefono ed email."
|
? "Data e luogo di nascita, telefono ed email."
|
||||||
: "Documento e certificato medico servono per il tesseramento CSI."}
|
: "Documento e certificato medico servono per il tesseramento CSI."}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AlertCircle } from "lucide-react";
|
import { Volleyball } from "lucide-react";
|
||||||
|
import { Avviso } from "@/components/crapp/Avviso";
|
||||||
import { formatData } from "@/lib/crapp-data";
|
import { formatData } from "@/lib/crapp-data";
|
||||||
import { eventiPalloni, eventoPrecedente, eventoSuccessivo, oggiISO } from "@/lib/palloni-core";
|
import { eventiPalloni, eventoPrecedente, eventoSuccessivo, oggiISO } from "@/lib/palloni-core";
|
||||||
import { useTurniPalloni } from "@/lib/palloni";
|
import { useTurniPalloni } from "@/lib/palloni";
|
||||||
@@ -17,6 +18,8 @@ export function PromemoriaPalloni() {
|
|||||||
|
|
||||||
const oggi = oggiISO();
|
const oggi = oggiISO();
|
||||||
const messaggi: string[] = [];
|
const messaggi: string[] = [];
|
||||||
|
// «Oggi» solo se c'è davvero qualcosa da fare oggi; altrimenti è un semplice promemoria.
|
||||||
|
let oggiTocca = true;
|
||||||
|
|
||||||
for (const evento of lista) {
|
for (const evento of lista) {
|
||||||
if (evento.data !== oggi) continue;
|
if (evento.data !== oggi) continue;
|
||||||
@@ -36,6 +39,7 @@ export function PromemoriaPalloni() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (messaggi.length === 0) {
|
if (messaggi.length === 0) {
|
||||||
|
oggiTocca = false;
|
||||||
const prossimo = lista.find((e) => e.data >= oggi && turni[e.id] === io.id);
|
const prossimo = lista.find((e) => e.data >= oggi && turni[e.id] === io.id);
|
||||||
if (!prossimo) return null;
|
if (!prossimo) return null;
|
||||||
messaggi.push(
|
messaggi.push(
|
||||||
@@ -44,15 +48,13 @@ export function PromemoriaPalloni() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-5 mt-4 rounded-3xl bg-accent-grad p-4 text-accent-foreground shadow-pop">
|
<Avviso
|
||||||
<p className="flex items-center gap-2 font-display text-lg uppercase leading-none">
|
tono="azione"
|
||||||
<AlertCircle className="h-5 w-5" /> Turno palloni
|
icona={Volleyball}
|
||||||
</p>
|
titolo="Turno palloni"
|
||||||
{messaggi.map((m) => (
|
etichetta={oggiTocca ? "Oggi" : "In arrivo"}
|
||||||
<p key={m} className="mt-2 text-xs leading-snug opacity-90">
|
testi={messaggi}
|
||||||
{m}
|
riquadri
|
||||||
</p>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export function RosaPresenze({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-baseline justify-between">
|
<div className="flex items-baseline justify-between">
|
||||||
<p className="text-sm font-bold">
|
<p className="text-[15px] font-bold">
|
||||||
Hanno risposto {risposteN}/{rosa.length}
|
Hanno risposto {risposteN}/{rosa.length}
|
||||||
</p>
|
</p>
|
||||||
<span className="font-display text-xl leading-none">{perc}%</span>
|
<span className="font-display text-xl leading-none">{perc}%</span>
|
||||||
@@ -84,7 +84,7 @@ export function RosaPresenze({
|
|||||||
<span
|
<span
|
||||||
key={s}
|
key={s}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-2.5 py-1 text-xs font-bold",
|
"rounded-full px-2.5 py-1 text-[13px] font-bold",
|
||||||
n > 0 ? statoMeta[s].className : "bg-secondary text-muted-foreground",
|
n > 0 ? statoMeta[s].className : "bg-secondary text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -92,14 +92,14 @@ export function RosaPresenze({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<span className="rounded-full bg-secondary px-2.5 py-1 text-xs font-bold text-muted-foreground">
|
<span className="rounded-full bg-secondary px-2.5 py-1 text-[13px] font-bold text-muted-foreground">
|
||||||
❔ {mancanti.length} da rispondere
|
❔ {mancanti.length} da rispondere
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{io ? (
|
{io ? (
|
||||||
<div className="mt-4 border-t border-border pt-3">
|
<div className="mt-4 border-t border-border pt-3">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
La tua risposta
|
La tua risposta
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
@@ -114,7 +114,7 @@ export function RosaPresenze({
|
|||||||
salva.mutate({ eventoId, giocatoreId: io.id, stato: attivo ? null : s })
|
salva.mutate({ eventoId, giocatoreId: io.id, stato: attivo ? null : s })
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full border border-border px-2.5 py-1.5 text-xs font-semibold transition-all active:scale-95 disabled:opacity-50",
|
"rounded-full border border-border px-2.5 py-1.5 text-[13px] font-semibold transition-all active:scale-95 disabled:opacity-50",
|
||||||
attivo
|
attivo
|
||||||
? cn(statoMeta[s].className, "border-transparent shadow-card")
|
? cn(statoMeta[s].className, "border-transparent shadow-card")
|
||||||
: "bg-background text-muted-foreground",
|
: "bg-background text-muted-foreground",
|
||||||
@@ -133,7 +133,7 @@ export function RosaPresenze({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={sollecita}
|
onClick={sollecita}
|
||||||
disabled={sollecito || daSollecitare === 0 || passato}
|
disabled={sollecito || daSollecitare === 0 || passato}
|
||||||
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-primary px-4 py-3 text-sm font-bold text-primary-foreground disabled:opacity-50"
|
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-primary px-4 py-3 text-[15px] font-bold text-primary-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{sollecito ? (
|
{sollecito ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
@@ -146,7 +146,7 @@ export function RosaPresenze({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<p aria-busy="true" className="text-center text-xs text-muted-foreground">
|
<p aria-busy="true" className="text-center text-[13px] text-muted-foreground">
|
||||||
Carico le risposte…
|
Carico le risposte…
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -194,16 +194,16 @@ function Gruppo({
|
|||||||
attenzione && "border border-dashed border-warning",
|
attenzione && "border border-dashed border-warning",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="flex items-center gap-1.5 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
{titolo} · {n}
|
{titolo} · {n}
|
||||||
{attenzione ? <HelpCircle className="h-3.5 w-3.5 text-warning" /> : null}
|
{attenzione ? <HelpCircle className="h-3.5 w-3.5 text-warning" /> : null}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
{lista.map((g) => (
|
{lista.map((g) => (
|
||||||
<div key={g.id} className="flex items-center gap-3">
|
<div key={g.id} className="flex items-center gap-3">
|
||||||
<Avatar id={g.id} fallback={String(g.numero)} className="h-8 w-8 text-xs" />
|
<Avatar id={g.id} fallback={String(g.numero)} className="h-8 w-8 text-[13px]" />
|
||||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{g.nome}</span>
|
<span className="min-w-0 flex-1 truncate text-[15px] font-semibold">{g.nome}</span>
|
||||||
<span className="shrink-0 text-xs text-muted-foreground">{g.ruolo}</span>
|
<span className="shrink-0 text-[13px] text-muted-foreground">{g.ruolo}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ScoutEntry({
|
|||||||
{occupato ? <Lock className="h-5 w-5 shrink-0" /> : <Radio className="h-5 w-5 shrink-0" />}
|
{occupato ? <Lock className="h-5 w-5 shrink-0" /> : <Radio className="h-5 w-5 shrink-0" />}
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block font-display text-lg uppercase leading-none">{titolo}</span>
|
<span className="block font-display text-lg uppercase leading-none">{titolo}</span>
|
||||||
<span className="block text-xs opacity-80">{sottotitolo}</span>
|
<span className="block text-[13px] opacity-80">{sottotitolo}</span>
|
||||||
</span>
|
</span>
|
||||||
{disponibile ? <ChevronRight className="h-5 w-5 shrink-0" /> : null}
|
{disponibile ? <ChevronRight className="h-5 w-5 shrink-0" /> : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export function SerieGriglia({ g }: { g: Giocatore }) {
|
|||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-bold leading-tight">{s.def.label}</p>
|
<p className="text-[15px] font-bold leading-tight">{s.def.label}</p>
|
||||||
<p className="text-xs text-muted-foreground">{s.def.descrizione}</p>
|
<p className="text-[13px] text-muted-foreground">{s.def.descrizione}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="inline-flex items-center gap-1 font-display text-2xl leading-none">
|
<span className="inline-flex items-center gap-1 font-display text-2xl leading-none">
|
||||||
<Flame
|
<Flame
|
||||||
@@ -39,7 +39,7 @@ export function SerieGriglia({ g }: { g: Giocatore }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Barra percentuale={s.progresso} trackClassName="mt-3" />
|
<Barra percentuale={s.progresso} trackClassName="mt-3" />
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
{s.prossimo ? `${s.valore}/${s.prossimo} · ` : ""}
|
{s.prossimo ? `${s.valore}/${s.prossimo} · ` : ""}
|
||||||
{s.messaggio}
|
{s.messaggio}
|
||||||
</p>
|
</p>
|
||||||
@@ -61,17 +61,17 @@ export function SerieHome({ g }: { g: Giocatore }) {
|
|||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-sm font-bold leading-tight">
|
<p className="text-[15px] font-bold leading-tight">
|
||||||
Serie {top.def.label.toLowerCase()}: {top.valore}
|
Serie {top.def.label.toLowerCase()}: {top.valore}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">{top.messaggio}</p>
|
<p className="text-[13px] text-muted-foreground">{top.messaggio}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||||
{serieGiocatore(g).map((s) => (
|
{serieGiocatore(g).map((s) => (
|
||||||
<div key={s.def.tipo} className="rounded-2xl bg-secondary p-2 text-center">
|
<div key={s.def.tipo} className="rounded-2xl bg-secondary p-2 text-center">
|
||||||
<p className="font-display text-xl leading-none">{s.valore}</p>
|
<p className="font-display text-xl leading-none">{s.valore}</p>
|
||||||
<p className="mt-1 text-xs font-semibold uppercase text-muted-foreground">
|
<p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground">
|
||||||
{s.def.label}
|
{s.def.label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ export function SondaggioCacche({
|
|||||||
if (!sondaggioAperto(dataEvento, oraEvento)) {
|
if (!sondaggioAperto(dataEvento, oraEvento)) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
💩 Sondaggio pre-partita
|
💩 Sondaggio pre-partita
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-[13px] text-muted-foreground">
|
||||||
{sondaggioTerminato(dataEvento, oraEvento)
|
{sondaggioTerminato(dataEvento, oraEvento)
|
||||||
? "Sondaggio chiuso: era aperto fino al fischio d'inizio."
|
? "Sondaggio chiuso: era aperto fino al fischio d'inizio."
|
||||||
: "Apre alle 8:00 del giorno della partita: riceverai una notifica."}
|
: "Apre alle 8:00 del giorno della partita: riceverai una notifica."}
|
||||||
@@ -96,14 +96,14 @@ export function SondaggioCacche({
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
💩 Sondaggio pre-partita
|
💩 Sondaggio pre-partita
|
||||||
</p>
|
</p>
|
||||||
<span className="rounded-full bg-secondary px-2.5 py-1 text-xs font-bold uppercase text-muted-foreground">
|
<span className="rounded-full bg-secondary px-2.5 py-1 text-[13px] font-bold uppercase text-muted-foreground">
|
||||||
{dellaPartita.length} risposte
|
{dellaPartita.length} risposte
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-[13px] text-muted-foreground">
|
||||||
Quante cacche hai fatto prima di questa partita? Dato scientifico fondamentale.
|
Quante cacche hai fatto prima di questa partita? Dato scientifico fondamentale.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ export function SondaggioCacche({
|
|||||||
disabled={!io || salva.isPending}
|
disabled={!io || salva.isPending}
|
||||||
onClick={() => rispondi(n)}
|
onClick={() => rispondi(n)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-11 rounded-xl px-3 py-2 text-sm font-bold tabular-nums transition-transform active:scale-90 disabled:opacity-50",
|
"min-w-11 rounded-xl px-3 py-2 text-[15px] font-bold tabular-nums transition-transform active:scale-90 disabled:opacity-50",
|
||||||
mia?.quantita === n
|
mia?.quantita === n
|
||||||
? "bg-accent text-accent-foreground shadow-pop"
|
? "bg-accent text-accent-foreground shadow-pop"
|
||||||
: "bg-secondary text-foreground",
|
: "bg-secondary text-foreground",
|
||||||
@@ -126,7 +126,7 @@ export function SondaggioCacche({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs">
|
<div className="mt-3 flex flex-wrap items-center gap-2 text-[13px]">
|
||||||
<span className="rounded-full bg-secondary px-2.5 py-1 font-semibold">
|
<span className="rounded-full bg-secondary px-2.5 py-1 font-semibold">
|
||||||
Media squadra {media}
|
Media squadra {media}
|
||||||
</span>
|
</span>
|
||||||
@@ -145,7 +145,7 @@ export function SondaggioCacche({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={avvisaTutti}
|
onClick={avvisaTutti}
|
||||||
disabled={avviso}
|
disabled={avviso}
|
||||||
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-primary px-4 py-3 text-sm font-bold text-primary-foreground disabled:opacity-50"
|
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-primary px-4 py-3 text-[15px] font-bold text-primary-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{avviso ? <Loader2 className="h-4 w-4 animate-spin" /> : <BellRing className="h-4 w-4" />}
|
{avviso ? <Loader2 className="h-4 w-4 animate-spin" /> : <BellRing className="h-4 w-4" />}
|
||||||
Avvisa tutti del sondaggio
|
Avvisa tutti del sondaggio
|
||||||
|
|||||||
@@ -74,16 +74,16 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) {
|
|||||||
<CircleDot className="h-4 w-4" />
|
<CircleDot className="h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<span className="block text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Palloni
|
Palloni
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1.5 text-sm font-bold leading-tight">
|
<span className="flex items-center gap-1.5 text-[15px] font-bold leading-tight">
|
||||||
{isPending || assegna.isPending ? (
|
{isPending || assegna.isPending ? (
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
|
||||||
) : null}
|
) : null}
|
||||||
<span className="truncate">{giocatore ? nomeCompleto(giocatore) : "Da assegnare"}</span>
|
<span className="truncate">{giocatore ? nomeCompleto(giocatore) : "Da assegnare"}</span>
|
||||||
{proposto && giocatore ? (
|
{proposto && giocatore ? (
|
||||||
<span className="shrink-0 rounded-full bg-card px-1.5 py-0.5 text-xs font-bold uppercase text-muted-foreground">
|
<span className="shrink-0 rounded-full bg-card px-1.5 py-0.5 text-[13px] font-bold uppercase text-muted-foreground">
|
||||||
proposto
|
proposto
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -100,11 +100,11 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => scegli(g.id)}
|
onClick={() => scegli(g.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors",
|
"flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[15px] transition-colors",
|
||||||
g.id === id ? "bg-accent/10 font-bold" : "hover:bg-secondary",
|
g.id === id ? "bg-accent/10 font-bold" : "hover:bg-secondary",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Avatar id={g.id} fallback={String(g.numero)} className="h-7 w-7 text-xs" />
|
<Avatar id={g.id} fallback={String(g.numero)} className="h-7 w-7 text-[13px]" />
|
||||||
<span className="min-w-0 flex-1 truncate">{nomeCompleto(g)}</span>
|
<span className="min-w-0 flex-1 truncate">{nomeCompleto(g)}</span>
|
||||||
{g.id === id ? <Check className="h-4 w-4 shrink-0 text-accent" /> : null}
|
{g.id === id ? <Check className="h-4 w-4 shrink-0 text-accent" /> : null}
|
||||||
</button>
|
</button>
|
||||||
@@ -117,7 +117,7 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={avvisa}
|
onClick={avvisa}
|
||||||
disabled={avviso}
|
disabled={avviso}
|
||||||
className="premi mt-2.5 flex w-full items-center justify-center gap-2 rounded-xl bg-card px-3 py-2 text-xs font-bold uppercase text-foreground disabled:opacity-50"
|
className="premi mt-2.5 flex w-full items-center justify-center gap-2 rounded-xl bg-card px-3 py-2 text-[13px] font-bold uppercase text-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{avviso ? (
|
{avviso ? (
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ export function VotazioneMvp({ evento }: { evento: Evento }) {
|
|||||||
if (!votoMvpAperto(evento.data, evento.ora)) {
|
if (!votoMvpAperto(evento.data, evento.ora)) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 rounded-2xl bg-secondary/60 p-3">
|
<div className="mt-3 rounded-2xl bg-secondary/60 p-3">
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="inline-flex items-center gap-1.5 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
<Vote className="h-3.5 w-3.5" /> Voto MVP
|
<Vote className="h-3.5 w-3.5" /> Voto MVP
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-[13px] text-muted-foreground">
|
||||||
Apre {ORE_ATTESA_MVP} ore dopo l'inizio della partita.
|
Apre {ORE_ATTESA_MVP} ore dopo l'inizio della partita.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,20 +75,20 @@ export function VotazioneMvp({ evento }: { evento: Evento }) {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-3 rounded-2xl bg-secondary/60 p-3">
|
<div className="mt-3 rounded-2xl bg-secondary/60 p-3">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="inline-flex items-center gap-1.5 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
<Vote className="h-3.5 w-3.5" /> Voto MVP · {totale} {totale === 1 ? "voto" : "voti"}
|
<Vote className="h-3.5 w-3.5" /> Voto MVP · {totale} {totale === 1 ? "voto" : "voti"}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAperto((v) => !v)}
|
onClick={() => setAperto((v) => !v)}
|
||||||
disabled={!puoVotare}
|
disabled={!puoVotare}
|
||||||
className="rounded-full bg-accent px-3 py-1 text-xs font-bold uppercase text-accent-foreground disabled:opacity-50"
|
className="rounded-full bg-accent px-3 py-1 text-[13px] font-bold uppercase text-accent-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{mio ? "Cambia voto" : "Vota"}
|
{mio ? "Cambia voto" : "Vota"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-2 text-xs">
|
<p className="mt-2 text-[13px]">
|
||||||
{testa && !pareggio ? (
|
{testa && !pareggio ? (
|
||||||
<span className="inline-flex items-center gap-1 font-bold">
|
<span className="inline-flex items-center gap-1 font-bold">
|
||||||
<Crown className="h-3.5 w-3.5 text-warning" /> {testa.nome} ({testa.voti})
|
<Crown className="h-3.5 w-3.5 text-warning" /> {testa.nome} ({testa.voti})
|
||||||
@@ -100,9 +100,9 @@ export function VotazioneMvp({ evento }: { evento: Evento }) {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{mio ? (
|
{mio ? (
|
||||||
<p className="mt-1 text-xs text-muted-foreground">Hai votato {mio.votato_nome}</p>
|
<p className="mt-1 text-[13px] text-muted-foreground">Hai votato {mio.votato_nome}</p>
|
||||||
) : !puoVotare ? (
|
) : !puoVotare ? (
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-[13px] text-muted-foreground">
|
||||||
Vota chi era presente a questa partita.
|
Vota chi era presente a questa partita.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -120,7 +120,7 @@ export function VotazioneMvp({ evento }: { evento: Evento }) {
|
|||||||
disabled={vota.isPending}
|
disabled={vota.isPending}
|
||||||
onClick={() => invia(g.id, nomeCompleto(g))}
|
onClick={() => invia(g.id, nomeCompleto(g))}
|
||||||
className={cn(
|
className={cn(
|
||||||
"truncate rounded-xl px-2.5 py-2 text-left text-xs font-semibold transition-colors",
|
"truncate rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold transition-colors",
|
||||||
mio?.votato_id === g.id
|
mio?.votato_id === g.id
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "bg-card text-foreground",
|
: "bg-card text-foreground",
|
||||||
@@ -133,7 +133,7 @@ export function VotazioneMvp({ evento }: { evento: Evento }) {
|
|||||||
) : conteggio.length > 0 ? (
|
) : conteggio.length > 0 ? (
|
||||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
{conteggio.map((c) => (
|
{conteggio.map((c) => (
|
||||||
<span key={c.id} className="rounded-lg bg-card px-2 py-1 text-xs font-semibold">
|
<span key={c.id} className="rounded-lg bg-card px-2 py-1 text-[13px] font-semibold">
|
||||||
{c.nome} · {c.voti}
|
{c.nome} · {c.voti}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function VotoSocial({ matchId }: { matchId: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="rounded-2xl bg-secondary/60 px-3 py-2 text-xs font-semibold text-muted-foreground">
|
<div className="rounded-2xl bg-secondary/60 px-3 py-2 text-[13px] font-semibold text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<Sparkles className="h-3.5 w-3.5 text-accent" />
|
<Sparkles className="h-3.5 w-3.5 text-accent" />
|
||||||
Hai votato {fatti}/{categorieSocial.length} categorie · un solo voto per categoria
|
Hai votato {fatti}/{categorieSocial.length} categorie · un solo voto per categoria
|
||||||
@@ -79,14 +79,16 @@ export function VotoSocial({ matchId }: { matchId: string }) {
|
|||||||
{cat.emoji}
|
{cat.emoji}
|
||||||
</span>
|
</span>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-bold leading-tight">{cat.nome}</span>
|
<span className="block truncate text-[15px] font-bold leading-tight">
|
||||||
<span className="block truncate text-xs text-muted-foreground">
|
{cat.nome}
|
||||||
|
</span>
|
||||||
|
<span className="block truncate text-[13px] text-muted-foreground">
|
||||||
{mio ? `Hai votato ${mio.votato_nome}` : cat.descrizione}
|
{mio ? `Hai votato ${mio.votato_nome}` : cat.descrizione}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
|
"shrink-0 rounded-full px-2.5 py-1 text-[13px] font-bold uppercase",
|
||||||
mio ? "bg-success text-success-foreground" : "bg-accent text-accent-foreground",
|
mio ? "bg-success text-success-foreground" : "bg-accent text-accent-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -105,7 +107,7 @@ export function VotoSocial({ matchId }: { matchId: string }) {
|
|||||||
disabled={vota.isPending}
|
disabled={vota.isPending}
|
||||||
onClick={() => invia(cat.id, g.id, nomeCompleto(g))}
|
onClick={() => invia(cat.id, g.id, nomeCompleto(g))}
|
||||||
className={cn(
|
className={cn(
|
||||||
"truncate rounded-xl px-2.5 py-2 text-left text-xs font-semibold transition-colors",
|
"truncate rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold transition-colors",
|
||||||
mio?.votato_id === g.id
|
mio?.votato_id === g.id
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "bg-secondary text-foreground",
|
: "bg-secondary text-foreground",
|
||||||
@@ -118,17 +120,17 @@ export function VotoSocial({ matchId }: { matchId: string }) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="border-t border-border px-3 py-2">
|
<div className="border-t border-border px-3 py-2">
|
||||||
{totale === 0 ? (
|
{totale === 0 ? (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
Nessun voto ancora: apri e scegli un compagno.
|
Nessun voto ancora: apri e scegli un compagno.
|
||||||
</p>
|
</p>
|
||||||
) : vincitore ? (
|
) : vincitore ? (
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs font-bold">
|
<p className="inline-flex items-center gap-1.5 text-[13px] font-bold">
|
||||||
<Crown className="h-3.5 w-3.5 text-oro" />
|
<Crown className="h-3.5 w-3.5 text-oro" />
|
||||||
<Icon className="h-3.5 w-3.5 text-accent" />
|
<Icon className="h-3.5 w-3.5 text-accent" />
|
||||||
{vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"}
|
{vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
Parità con {totale} voti: servono altri voti per assegnare il badge.
|
Parità con {totale} voti: servono altri voti per assegnare il badge.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -107,13 +107,13 @@ function RigaNotifica({
|
|||||||
const arrivo = info.offset.x + proietta(info.velocity.x);
|
const arrivo = info.offset.x + proietta(info.velocity.x);
|
||||||
if (arrivo < -60) onElimina(notifica.id);
|
if (arrivo < -60) onElimina(notifica.id);
|
||||||
}}
|
}}
|
||||||
className="touch-pan-y bg-card p-3 pr-8 text-sm"
|
className="touch-pan-y bg-card p-3 pr-8 text-[15px]"
|
||||||
>
|
>
|
||||||
<p className="font-semibold">{notifica.titolo}</p>
|
<p className="font-semibold">{notifica.titolo}</p>
|
||||||
{notifica.corpo ? (
|
{notifica.corpo ? (
|
||||||
<p className="mt-0.5 whitespace-pre-line text-muted-foreground">{notifica.corpo}</p>
|
<p className="mt-0.5 whitespace-pre-line text-muted-foreground">{notifica.corpo}</p>
|
||||||
) : null}
|
) : null}
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{tempoRelativo(notifica.creataIl)}</p>
|
<p className="mt-1 text-[13px] text-muted-foreground">{tempoRelativo(notifica.creataIl)}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -254,7 +254,7 @@ export function PageHeader({
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="truncate font-display-lg text-3xl uppercase">{titolo}</h1>
|
<h1 className="truncate font-display-lg text-3xl uppercase">{titolo}</h1>
|
||||||
{sottotitolo ? (
|
{sottotitolo ? (
|
||||||
<p className="mt-1 truncate text-sm text-primary-foreground/80">{sottotitolo}</p>
|
<p className="mt-1 truncate text-[15px] text-primary-foreground/80">{sottotitolo}</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{azione ?? <LinkProfilo />}
|
{azione ?? <LinkProfilo />}
|
||||||
@@ -338,7 +338,7 @@ export function SezioneTendina({
|
|||||||
* parità di padding — un'altezza esplicita allinea tutti i campi tra loro.
|
* parità di padding — un'altezza esplicita allinea tutti i campi tra loro.
|
||||||
*/
|
*/
|
||||||
export const classiInput =
|
export const classiInput =
|
||||||
"h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-sm";
|
"h-10 w-full min-w-0 rounded-xl border border-border bg-background px-3 py-2 text-[15px]";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label
|
* Etichetta + controllo di un form. `min-w-0`: dentro `grid-cols-2` questa label
|
||||||
@@ -349,7 +349,7 @@ export const classiInput =
|
|||||||
export function Campo({ label, children }: { label: string; children: ReactNode }) {
|
export function Campo({ label, children }: { label: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<label className="block min-w-0">
|
<label className="block min-w-0">
|
||||||
<span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<span className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 block">{children}</span>
|
<span className="mt-1 block">{children}</span>
|
||||||
@@ -378,7 +378,7 @@ export function StatoBadge({ stato, className }: { stato: Stato; className?: str
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
|
"inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[13px] font-bold uppercase",
|
||||||
meta.className,
|
meta.className,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -402,10 +402,10 @@ export function StatTile({
|
|||||||
<p className="font-display text-2xl leading-none">
|
<p className="font-display text-2xl leading-none">
|
||||||
{typeof valore === "number" ? <Numero valore={valore} /> : valore}
|
{typeof valore === "number" ? <Numero valore={valore} /> : valore}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
<p className="mt-1 text-[13px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
{hint ? <p className="mt-0.5 text-xs text-accent">{hint}</p> : null}
|
{hint ? <p className="mt-0.5 text-[13px] text-accent">{hint}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ const DrawerDescription = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DrawerPrimitive.Description
|
<DrawerPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-[15px] text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -222,40 +222,6 @@ export function useSalvaDatiSquadra() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Numero e data della tessera CSI, note solo dopo il tesseramento effettivo. */
|
|
||||||
export type DatiTesseramento = Pick<GiocatoreSquadra, "numeroTessera" | "dataTessera">;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registra numero e data della tessera CSI (roadmap v1.1). Campo puramente amministrativo:
|
|
||||||
* il trigger di M8 lo rende scrivibile solo da un admin, il giocatore non può autodichiararsi
|
|
||||||
* tesserato.
|
|
||||||
*/
|
|
||||||
export function useSalvaTesseramento() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (input: { giocatoreId: string; dati: DatiTesseramento }) => {
|
|
||||||
const dati = {
|
|
||||||
numero_tessera: input.dati.numeroTessera?.trim() || null,
|
|
||||||
data_tessera: input.dati.dataTessera || null,
|
|
||||||
};
|
|
||||||
const { error } = await supabaseNuoveTabelle
|
|
||||||
.from("giocatori_squadra")
|
|
||||||
.update(dati)
|
|
||||||
.eq("id", input.giocatoreId);
|
|
||||||
if (error) throw error;
|
|
||||||
return {
|
|
||||||
giocatoreId: input.giocatoreId,
|
|
||||||
dati: { numeroTessera: dati.numero_tessera, dataTessera: dati.data_tessera },
|
|
||||||
};
|
|
||||||
},
|
|
||||||
onSuccess: (input) => {
|
|
||||||
queryClient.setQueryData<GiocatoreSquadra[]>(SQUADRA_KEY, (prec) =>
|
|
||||||
(prec ?? []).map((g) => (g.id === input.giocatoreId ? { ...g, ...input.dati } : g)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggiunge un giocatore alla rosa (DD-017). Solo un admin passa le policy di M1.
|
* Aggiunge un giocatore alla rosa (DD-017). Solo un admin passa le policy di M1.
|
||||||
* L'id (`g<N>`) non è generato dal database: va calcolato con `prossimoIdGiocatore`
|
* L'id (`g<N>`) non è generato dal database: va calcolato con `prossimoIdGiocatore`
|
||||||
|
|||||||
+239
-17
@@ -14,6 +14,25 @@ export type ObiettivoSquadra = {
|
|||||||
emoji: string;
|
emoji: string;
|
||||||
/** Frase breve che spiega come ogni giocatore può spostare l'ago. */
|
/** Frase breve che spiega come ogni giocatore può spostare l'ago. */
|
||||||
impatto: string;
|
impatto: string;
|
||||||
|
/** Spiegazione completa mostrata nella card di dettaglio: nessun punto oscuro. */
|
||||||
|
dettaglio: DettaglioObiettivo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DettaglioObiettivo = {
|
||||||
|
/** Cosa misura l'obiettivo e come si arriva al numero mostrato, in parole semplici. */
|
||||||
|
comeSiCalcola: string;
|
||||||
|
/** Cosa fa salire il numero. */
|
||||||
|
conta: string[];
|
||||||
|
/** Cosa è una presenza persa: occupa un posto ma lo lascia vuoto, quindi abbassa la percentuale. */
|
||||||
|
perse?: string[];
|
||||||
|
/** Cosa resta fuori dal calcolo: non sale e non scende. */
|
||||||
|
nonConta: string[];
|
||||||
|
/** Quando l'obiettivo parte, quando si azzera o scade. */
|
||||||
|
periodo: string;
|
||||||
|
/** Da dove arrivano i dati e quando si aggiornano. */
|
||||||
|
fonte: string;
|
||||||
|
/** Esempio concreto con numeri. */
|
||||||
|
esempio: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContestoObiettivi = {
|
export type ContestoObiettivi = {
|
||||||
@@ -31,6 +50,24 @@ function meseCorrente(oggi: Date): string {
|
|||||||
return new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(oggi).slice(0, 7);
|
return new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(oggi).slice(0, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Istante corrente in Europe/Rome come "YYYY-MM-DD HH:mm", confrontabile con data + ora di un evento. */
|
||||||
|
function adessoRoma(oggi: Date): string {
|
||||||
|
const p = Object.fromEntries(
|
||||||
|
new Intl.DateTimeFormat("en-CA", {
|
||||||
|
timeZone: "Europe/Rome",
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hourCycle: "h23",
|
||||||
|
})
|
||||||
|
.formatToParts(oggi)
|
||||||
|
.map((x) => [x.type, x.value]),
|
||||||
|
);
|
||||||
|
return `${p["year"]}-${p["month"]}-${p["day"]} ${p["hour"]}:${p["minute"]}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** "a settembre" / "ad agosto": preposizione con elisione davanti a vocale. */
|
/** "a settembre" / "ad agosto": preposizione con elisione davanti a vocale. */
|
||||||
function aMese(oggi: Date): string {
|
function aMese(oggi: Date): string {
|
||||||
const nome = new Intl.DateTimeFormat("it-IT", { timeZone: "Europe/Rome", month: "long" }).format(
|
const nome = new Intl.DateTimeFormat("it-IT", { timeZone: "Europe/Rome", month: "long" }).format(
|
||||||
@@ -49,16 +86,30 @@ function fineMese(oggi: Date): string {
|
|||||||
return `${mese}-${String(ultimoGiorno).padStart(2, "0")}`;
|
return `${mese}-${String(ultimoGiorno).padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function percentualePresenzeMese(ctx: ContestoObiettivi, rosaSize: number, mese: string) {
|
/**
|
||||||
const delMese = ctx.eventi.filter(
|
* Presenze (presente o ritardo) sul totale dei posti degli allenamenti e delle partite scelti da
|
||||||
(e) => e.data.startsWith(mese) && (e.tipo === "partita" || e.tipo === "allenamento"),
|
* `filtro`: per ogni evento i posti sono i convocati, o tutta la rosa se non ce ne sono. Chi non
|
||||||
);
|
* è convocato non occupa un posto, quindi la sua assenza non pesa.
|
||||||
if (delMese.length === 0 || rosaSize === 0) return 0;
|
*/
|
||||||
const posti = delMese.length * rosaSize;
|
function percentualePresenze(
|
||||||
const presenti = delMese.reduce((s, e) => {
|
ctx: ContestoObiettivi,
|
||||||
|
rosa: Giocatore[],
|
||||||
|
filtro: (e: Evento) => boolean = () => true,
|
||||||
|
) {
|
||||||
|
let posti = 0;
|
||||||
|
let presenti = 0;
|
||||||
|
for (const e of ctx.eventi) {
|
||||||
|
if (e.tipo !== "partita" && e.tipo !== "allenamento") continue;
|
||||||
|
if (!filtro(e)) continue;
|
||||||
|
const attesi = e.convocati.length === 0 ? rosa : rosa.filter((g) => e.convocati.includes(g.id));
|
||||||
|
posti += attesi.length;
|
||||||
const risposte = ctx.presenze[e.id] ?? {};
|
const risposte = ctx.presenze[e.id] ?? {};
|
||||||
return s + Object.values(risposte).filter((x) => x === "presente" || x === "ritardo").length;
|
presenti += attesi.filter((g) => {
|
||||||
}, 0);
|
const stato = risposte[g.id];
|
||||||
|
return stato === "presente" || stato === "ritardo";
|
||||||
|
}).length;
|
||||||
|
}
|
||||||
|
if (posti === 0) return 0;
|
||||||
return Math.round((presenti / posti) * 100);
|
return Math.round((presenti / posti) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +130,6 @@ export function obiettiviSquadra(
|
|||||||
ctx: ContestoObiettivi = contestoVuoto,
|
ctx: ContestoObiettivi = contestoVuoto,
|
||||||
oggi: Date = new Date(),
|
oggi: Date = new Date(),
|
||||||
): ObiettivoSquadra[] {
|
): ObiettivoSquadra[] {
|
||||||
const somma = (f: (g: Giocatore) => number) => rosa.reduce((s, g) => s + f(g), 0);
|
|
||||||
const continui = rosa.filter((g) => g.serieAllenamenti >= 3).length;
|
const continui = rosa.filter((g) => g.serieAllenamenti >= 3).length;
|
||||||
const vittorie = ctx.vittorie ?? 0;
|
const vittorie = ctx.vittorie ?? 0;
|
||||||
const mese = meseCorrente(oggi);
|
const mese = meseCorrente(oggi);
|
||||||
@@ -88,12 +138,35 @@ export function obiettiviSquadra(
|
|||||||
id: "o1",
|
id: "o1",
|
||||||
titolo: `90% di presenze ${aMese(oggi)}`,
|
titolo: `90% di presenze ${aMese(oggi)}`,
|
||||||
descrizione: "Media presenze su partite e allenamenti del mese",
|
descrizione: "Media presenze su partite e allenamenti del mese",
|
||||||
valore: percentualePresenzeMese(ctx, rosa.length, mese),
|
valore: percentualePresenze(ctx, rosa, (e) => e.data.startsWith(mese)),
|
||||||
target: 90,
|
target: 90,
|
||||||
unita: "%",
|
unita: "%",
|
||||||
scadenza: fineMese(oggi),
|
scadenza: fineMese(oggi),
|
||||||
emoji: "📣",
|
emoji: "📣",
|
||||||
impatto: "Ogni sì in più alza la media di tutta la squadra.",
|
impatto: "Ogni sì in più alza la media di tutta la squadra.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si guardano solo le partite e gli allenamenti del mese in corso. Per ognuno i posti sono i giocatori convocati (se nessuno è stato convocato, tutta la rosa). Un posto è pieno se il giocatore ha risposto «presente» o «in ritardo», vuoto in tutti gli altri casi. La percentuale è posti pieni ÷ posti totali, sommando tutti gli eventi del mese.",
|
||||||
|
conta: [
|
||||||
|
"«Presente» o «in ritardo» di un convocato a una partita o a un allenamento del mese.",
|
||||||
|
"Vale anche per gli eventi futuri del mese: appena confermi, la percentuale sale.",
|
||||||
|
],
|
||||||
|
perse: [
|
||||||
|
"«Assente» di un convocato: è una presenza persa, il posto resta vuoto.",
|
||||||
|
"«Forse», «infortunato» o nessuna risposta di un convocato: finché non diventa «presente» o «in ritardo» il posto è vuoto.",
|
||||||
|
],
|
||||||
|
nonConta: [
|
||||||
|
"Chi non è convocato a quell'evento: non occupa un posto, quindi la sua assenza non pesa.",
|
||||||
|
"Cene, pizzate e altri eventi di squadra, e i compleanni: non sono partite né allenamenti e restano fuori da questa statistica.",
|
||||||
|
"Gli eventi degli altri mesi.",
|
||||||
|
],
|
||||||
|
periodo:
|
||||||
|
"Solo il mese in corso: si azzera il primo giorno di ogni mese e si chiude l'ultimo giorno del mese. Il mese dopo riparte da zero, con il suo titolo e la sua scadenza.",
|
||||||
|
fonte:
|
||||||
|
"Calendario (eventi e convocati) e risposte di presenza dei giocatori. Si aggiorna ogni volta che qualcuno risponde o cambia risposta.",
|
||||||
|
esempio:
|
||||||
|
"Ad agosto ci sono 2 allenamenti. Il primo ha 12 convocati e 10 rispondono «presente»: 10 posti pieni su 12. Il secondo ha 10 convocati e 9 «presenti»: 9 su 10. In tutto 19 posti pieni su 22, cioè 86%. I giocatori non convocati non sono nel conto: per arrivare al 90% servono almeno 20 posti pieni su 22.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o2",
|
id: "o2",
|
||||||
@@ -105,16 +178,55 @@ export function obiettiviSquadra(
|
|||||||
scadenza: fineMese(oggi),
|
scadenza: fineMese(oggi),
|
||||||
emoji: "⚡",
|
emoji: "⚡",
|
||||||
impatto: "Bastano pochi tap per far quadrare i conti a chi organizza.",
|
impatto: "Bastano pochi tap per far quadrare i conti a chi organizza.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Per ogni evento in calendario ci sono tanti posti quanti sono i giocatori in rosa. Si contano le risposte date, di qualsiasi tipo, e si divide per tutti i posti.",
|
||||||
|
conta: [
|
||||||
|
"Qualsiasi risposta: presente, in ritardo o assente. Conta aver risposto, non cosa si è risposto.",
|
||||||
|
"Partite, allenamenti ed eventi di squadra.",
|
||||||
|
],
|
||||||
|
nonConta: [
|
||||||
|
"I compleanni: non richiedono risposta.",
|
||||||
|
"Un evento a cui non hai ancora risposto.",
|
||||||
|
],
|
||||||
|
periodo:
|
||||||
|
"Considera tutti gli eventi in programma, non solo quelli del mese. La scadenza indicata è la fine del mese corrente.",
|
||||||
|
fonte: "Eventi del calendario e risposte dei giocatori. Si aggiorna a ogni risposta.",
|
||||||
|
esempio:
|
||||||
|
"Con 14 giocatori e 10 eventi i posti sono 140. Con 126 risposte date, siamo al 90%.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o7",
|
id: "o7",
|
||||||
titolo: "250 presenze complessive",
|
titolo: "Presenze collettive",
|
||||||
descrizione: "Somma delle presenze di tutta la rosa in stagione",
|
descrizione: "Presenze di tutta la rosa su allenamenti e partite definiti in stagione",
|
||||||
valore: somma((g) => g.presenze),
|
valore: percentualePresenze(ctx, rosa),
|
||||||
target: 250,
|
target: 90,
|
||||||
unita: "presenze",
|
unita: "%",
|
||||||
emoji: "🤝",
|
emoji: "🤝",
|
||||||
impatto: "Ogni allenamento a cui vieni vale +1 per il gruppo.",
|
impatto: "Ogni allenamento a cui vieni vale +1 per il gruppo.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si guardano tutte le partite e tutti gli allenamenti della stagione, passati e futuri. Per ognuno i posti sono i giocatori convocati (se nessuno è stato convocato, tutta la rosa). Un posto è pieno se il giocatore ha risposto «presente» o «in ritardo», vuoto in tutti gli altri casi. La percentuale è posti pieni ÷ posti totali, sommando tutti gli eventi insieme.",
|
||||||
|
conta: [
|
||||||
|
"«Presente» o «in ritardo» di un convocato a una partita o a un allenamento.",
|
||||||
|
"Anche le conferme per gli eventi futuri: appena confermi, la percentuale sale.",
|
||||||
|
],
|
||||||
|
perse: [
|
||||||
|
"«Assente» di un convocato: è una presenza persa, il posto resta vuoto.",
|
||||||
|
"«Forse», «infortunato» o nessuna risposta di un convocato: finché non diventa «presente» o «in ritardo» il posto è vuoto.",
|
||||||
|
],
|
||||||
|
nonConta: [
|
||||||
|
"Chi non è convocato a quell'evento: non occupa un posto, quindi la sua assenza non pesa.",
|
||||||
|
"Cene, pizzate e altri eventi di squadra, e i compleanni: non sono partite né allenamenti e restano fuori da questa statistica.",
|
||||||
|
],
|
||||||
|
periodo:
|
||||||
|
"Tutta la stagione: non si azzera mai e non ha scadenza. Conta tutto ciò che c'è in calendario, quindi un evento cancellato smette di contare e uno nuovo aggiunge i suoi posti.",
|
||||||
|
fonte:
|
||||||
|
"Calendario (eventi e convocati) e risposte di presenza dei giocatori. Si aggiorna ogni volta che qualcuno risponde o cambia risposta.",
|
||||||
|
esempio:
|
||||||
|
"In stagione ci sono 3 eventi: un allenamento con 12 convocati e 10 presenti, uno con 10 convocati e 9 presenti, una partita con 14 convocati e 13 presenti. Posti pieni 32 su 36, cioè 89%: per il 90% manca una presenza in più. La differenza con quello del mese è solo il periodo: qui contano tutti gli eventi insieme, senza azzerarsi.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o12",
|
id: "o12",
|
||||||
@@ -125,6 +237,19 @@ export function obiettiviSquadra(
|
|||||||
unita: "di media",
|
unita: "di media",
|
||||||
emoji: "📝",
|
emoji: "📝",
|
||||||
impatto: "Prestazioni di gruppo: la media sale se giochiamo da squadra.",
|
impatto: "Prestazioni di gruppo: la media sale se giochiamo da squadra.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Media di tutti i voti dati dai giocatori nelle pagelle dopo le partite, arrotondata a una cifra decimale.",
|
||||||
|
conta: ["Ogni voto dato in una pagella, a qualsiasi compagno."],
|
||||||
|
nonConta: [
|
||||||
|
"Gli autovoti: non si può votare se stessi.",
|
||||||
|
"Le pagelle non compilate: non pesano sulla media.",
|
||||||
|
],
|
||||||
|
periodo: "Tutta la stagione. Non si azzera e non ha scadenza.",
|
||||||
|
fonte: "Pagelle compilate dai giocatori. Si aggiorna a ogni voto.",
|
||||||
|
esempio:
|
||||||
|
"Se i voti dati finora sono 7, 8 e 7, la media è 7.3: mancano 0.2 per arrivare a 7.5.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o13",
|
id: "o13",
|
||||||
@@ -135,6 +260,16 @@ export function obiettiviSquadra(
|
|||||||
unita: "voti",
|
unita: "voti",
|
||||||
emoji: "🗳️",
|
emoji: "🗳️",
|
||||||
impatto: "Vota i compagni a fine partita: bastano due minuti.",
|
impatto: "Vota i compagni a fine partita: bastano due minuti.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si contano tutti i voti dati nelle pagelle durante la stagione. Un voto è un giocatore che valuta un compagno dopo una partita.",
|
||||||
|
conta: ["Ogni singolo voto dato a un compagno, non ogni pagella intera."],
|
||||||
|
nonConta: ["Le pagelle non compilate.", "Gli autovoti, che non sono ammessi."],
|
||||||
|
periodo: "Tutta la stagione. Non si azzera e non ha scadenza.",
|
||||||
|
fonte: "Pagelle compilate dai giocatori. Si aggiorna a ogni voto.",
|
||||||
|
esempio:
|
||||||
|
"Se dopo una partita 10 giocatori votano 9 compagni ciascuno, la squadra aggiunge 90 voti.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o11",
|
id: "o11",
|
||||||
@@ -145,6 +280,21 @@ export function obiettiviSquadra(
|
|||||||
unita: "giocatori",
|
unita: "giocatori",
|
||||||
emoji: "🔗",
|
emoji: "🔗",
|
||||||
impatto: "Tieni viva la tua serie e sblocchi anche questo.",
|
impatto: "Tieni viva la tua serie e sblocchi anche questo.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si conta quanti giocatori hanno una serie di almeno 3 allenamenti consecutivi senza saltarne nemmeno uno.",
|
||||||
|
conta: ["Un giocatore con 3 o più allenamenti di fila in cui era presente o in ritardo."],
|
||||||
|
nonConta: [
|
||||||
|
"Chi ha saltato un allenamento: la serie riparte da zero.",
|
||||||
|
"Un allenamento passato senza risposta vale come assenza e azzera la serie.",
|
||||||
|
"Le partite: la serie riguarda solo gli allenamenti.",
|
||||||
|
"Un allenamento saltato per infortunio non la azzera: la serie resta ferma al valore di prima.",
|
||||||
|
],
|
||||||
|
periodo: "Si aggiorna di continuo sugli allenamenti passati. Non ha scadenza.",
|
||||||
|
fonte: "Presenze agli allenamenti. Vedi anche la serie personale nel tuo profilo.",
|
||||||
|
esempio:
|
||||||
|
"Il target è 12 perché è il numero minimo per un allenamento 6 contro 6. Con 9 giocatori in serie ne mancano 3.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o3",
|
id: "o3",
|
||||||
@@ -155,6 +305,22 @@ export function obiettiviSquadra(
|
|||||||
unita: "vittorie",
|
unita: "vittorie",
|
||||||
emoji: "🎉",
|
emoji: "🎉",
|
||||||
impatto: "Una prestazione di gruppo e il primo passo è fatto.",
|
impatto: "Una prestazione di gruppo e il primo passo è fatto.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con la prima vittoria.",
|
||||||
|
conta: [
|
||||||
|
"Ogni partita ufficiale di campionato in cui abbiamo vinto più set degli avversari.",
|
||||||
|
],
|
||||||
|
nonConta: [
|
||||||
|
"Le sconfitte e le partite non ancora giocate.",
|
||||||
|
"Amichevoli e allenamenti.",
|
||||||
|
"Le vittorie oltre il target: il contatore si ferma al traguardo.",
|
||||||
|
],
|
||||||
|
periodo: "Tutta la stagione di campionato. Non si azzera.",
|
||||||
|
fonte:
|
||||||
|
"Risultati ufficiali del portale CSI Bologna. Si aggiorna quando il portale pubblica il risultato, quindi può esserci un ritardo.",
|
||||||
|
esempio: "Un 3-1 è una vittoria. Un 2-3 non lo è, anche se abbiamo vinto due set.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o4",
|
id: "o4",
|
||||||
@@ -165,6 +331,22 @@ export function obiettiviSquadra(
|
|||||||
unita: "vittorie",
|
unita: "vittorie",
|
||||||
emoji: "🔥",
|
emoji: "🔥",
|
||||||
impatto: "Ogni vittoria ci avvicina ai playoff.",
|
impatto: "Ogni vittoria ci avvicina ai playoff.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con 5 vittorie.",
|
||||||
|
conta: [
|
||||||
|
"Ogni partita ufficiale di campionato in cui abbiamo vinto più set degli avversari.",
|
||||||
|
],
|
||||||
|
nonConta: [
|
||||||
|
"Le sconfitte e le partite non ancora giocate.",
|
||||||
|
"Amichevoli e allenamenti.",
|
||||||
|
"Le vittorie oltre il target: il contatore si ferma al traguardo.",
|
||||||
|
],
|
||||||
|
periodo: "Tutta la stagione di campionato. Non si azzera.",
|
||||||
|
fonte:
|
||||||
|
"Risultati ufficiali del portale CSI Bologna. Si aggiorna quando il portale pubblica il risultato, quindi può esserci un ritardo.",
|
||||||
|
esempio: "Un 3-1 è una vittoria. Un 2-3 non lo è, anche se abbiamo vinto due set.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o5",
|
id: "o5",
|
||||||
@@ -175,16 +357,56 @@ export function obiettiviSquadra(
|
|||||||
unita: "vittorie",
|
unita: "vittorie",
|
||||||
emoji: "🏆",
|
emoji: "🏆",
|
||||||
impatto: "L'obiettivo grande: serve tutta la stagione insieme.",
|
impatto: "L'obiettivo grande: serve tutta la stagione insieme.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si contano le partite di campionato vinte dalla squadra: vince chi si aggiudica più set. L'obiettivo si completa con 10 vittorie.",
|
||||||
|
conta: [
|
||||||
|
"Ogni partita ufficiale di campionato in cui abbiamo vinto più set degli avversari.",
|
||||||
|
],
|
||||||
|
nonConta: [
|
||||||
|
"Le sconfitte e le partite non ancora giocate.",
|
||||||
|
"Amichevoli e allenamenti.",
|
||||||
|
"Le vittorie oltre il target: il contatore si ferma al traguardo.",
|
||||||
|
],
|
||||||
|
periodo: "Tutta la stagione di campionato. Non si azzera.",
|
||||||
|
fonte:
|
||||||
|
"Risultati ufficiali del portale CSI Bologna. Si aggiorna quando il portale pubblica il risultato, quindi può esserci un ritardo.",
|
||||||
|
esempio: "Un 3-1 è una vittoria. Un 2-3 non lo è, anche se abbiamo vinto due set.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "o6",
|
id: "o6",
|
||||||
titolo: "1 evento di squadra al mese",
|
titolo: "1 evento di squadra al mese",
|
||||||
descrizione: "Pizzate, cene e uscite fuori dal campo",
|
descrizione: "Pizzate, cene e uscite fuori dal campo",
|
||||||
valore: ctx.eventi.filter((e) => e.tipo === "evento" && e.data.startsWith(mese)).length,
|
// Conta solo quando l'ora dell'evento è arrivata, non quando viene definito.
|
||||||
|
valore: ctx.eventi.filter(
|
||||||
|
(e) =>
|
||||||
|
e.tipo === "evento" &&
|
||||||
|
e.data.startsWith(mese) &&
|
||||||
|
`${e.data} ${e.ora || "00:00"}` <= adessoRoma(oggi),
|
||||||
|
).length,
|
||||||
target: 1,
|
target: 1,
|
||||||
unita: "eventi",
|
unita: "eventi",
|
||||||
emoji: "🍕",
|
emoji: "🍕",
|
||||||
impatto: "Il gruppo si costruisce anche fuori dal campo.",
|
impatto: "Il gruppo si costruisce anche fuori dal campo.",
|
||||||
|
dettaglio: {
|
||||||
|
comeSiCalcola:
|
||||||
|
"Si conta quanti eventi di squadra del mese corrente sono già iniziati. Basta uno per completare l'obiettivo.",
|
||||||
|
conta: [
|
||||||
|
"Pizzate, cene e uscite fuori dal campo inserite nel calendario come «evento».",
|
||||||
|
"Solo quando l'ora dell'evento è arrivata. Se manca l'ora vale la mezzanotte.",
|
||||||
|
],
|
||||||
|
nonConta: [
|
||||||
|
"Un evento ancora nel futuro: creato oggi per la settimana prossima non conta ancora.",
|
||||||
|
"Partite e allenamenti.",
|
||||||
|
"Eventi di altri mesi.",
|
||||||
|
],
|
||||||
|
periodo:
|
||||||
|
"Si azzera il primo giorno di ogni mese. Il passaggio a completato compare al primo aggiornamento della pagina dopo l'ora dell'evento.",
|
||||||
|
fonte: "Eventi del calendario.",
|
||||||
|
esempio:
|
||||||
|
"Una pizzata fissata il 20 alle 20:30: fino a quell'ora l'obiettivo resta 0/1, dopo diventa 1/1.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ function NotFoundComponent() {
|
|||||||
<div className="max-w-md text-center">
|
<div className="max-w-md text-center">
|
||||||
<h1 className="font-display-lg text-7xl text-foreground">404</h1>
|
<h1 className="font-display-lg text-7xl text-foreground">404</h1>
|
||||||
<h2 className="mt-4 text-xl font-semibold text-foreground">Pagina non trovata</h2>
|
<h2 className="mt-4 text-xl font-semibold text-foreground">Pagina non trovata</h2>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-[15px] text-muted-foreground">
|
||||||
La pagina che cerchi non esiste o è stata spostata.
|
La pagina che cerchi non esiste o è stata spostata.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="premi inline-flex min-h-11 items-center justify-center rounded-2xl bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
className="premi inline-flex min-h-11 items-center justify-center rounded-2xl bg-primary px-4 py-2 text-[15px] font-semibold text-primary-foreground"
|
||||||
>
|
>
|
||||||
Torna alla home
|
Torna alla home
|
||||||
</Link>
|
</Link>
|
||||||
@@ -53,7 +53,7 @@ function ErrorComponent({ error, reset }: { error: unknown; reset: () => void })
|
|||||||
<h1 className="text-xl font-semibold tracking-tight text-foreground">
|
<h1 className="text-xl font-semibold tracking-tight text-foreground">
|
||||||
Questa pagina non si è caricata
|
Questa pagina non si è caricata
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-[15px] text-muted-foreground">
|
||||||
Qualcosa è andato storto da parte nostra. Puoi riprovare o tornare alla home.
|
Qualcosa è andato storto da parte nostra. Puoi riprovare o tornare alla home.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6 flex flex-wrap justify-center gap-2">
|
<div className="mt-6 flex flex-wrap justify-center gap-2">
|
||||||
@@ -63,13 +63,13 @@ function ErrorComponent({ error, reset }: { error: unknown; reset: () => void })
|
|||||||
router.invalidate();
|
router.invalidate();
|
||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
className="premi inline-flex min-h-11 items-center justify-center rounded-2xl bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground"
|
className="premi inline-flex min-h-11 items-center justify-center rounded-2xl bg-primary px-4 py-2 text-[15px] font-semibold text-primary-foreground"
|
||||||
>
|
>
|
||||||
Riprova
|
Riprova
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
className="premi inline-flex min-h-11 items-center justify-center rounded-2xl border border-border bg-card px-4 py-2 text-sm font-semibold text-foreground"
|
className="premi inline-flex min-h-11 items-center justify-center rounded-2xl border border-border bg-card px-4 py-2 text-[15px] font-semibold text-foreground"
|
||||||
>
|
>
|
||||||
Torna alla home
|
Torna alla home
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+24
-95
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
BadgeCheck,
|
|
||||||
Bell,
|
Bell,
|
||||||
BellOff,
|
BellOff,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -33,11 +32,9 @@ import {
|
|||||||
useGiocatoriSquadra,
|
useGiocatoriSquadra,
|
||||||
useImpostaAttivo,
|
useImpostaAttivo,
|
||||||
useSalvaDatiSquadra,
|
useSalvaDatiSquadra,
|
||||||
useSalvaTesseramento,
|
|
||||||
useScollegaAccount,
|
useScollegaAccount,
|
||||||
validaDatiSquadra,
|
validaDatiSquadra,
|
||||||
type DatiSquadra,
|
type DatiSquadra,
|
||||||
type DatiTesseramento,
|
|
||||||
type GiocatoreSquadra,
|
type GiocatoreSquadra,
|
||||||
} from "@/lib/giocatori-squadra";
|
} from "@/lib/giocatori-squadra";
|
||||||
import { scaricaFile, useProfili, useSalvaProfilo } from "@/lib/profili";
|
import { scaricaFile, useProfili, useSalvaProfilo } from "@/lib/profili";
|
||||||
@@ -110,18 +107,11 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
|||||||
const { righe } = useGiocatoriSquadra();
|
const { righe } = useGiocatoriSquadra();
|
||||||
const salvaSquadra = useSalvaDatiSquadra();
|
const salvaSquadra = useSalvaDatiSquadra();
|
||||||
const salvaProfilo = useSalvaProfilo();
|
const salvaProfilo = useSalvaProfilo();
|
||||||
const salvaTesseramento = useSalvaTesseramento();
|
|
||||||
const scollega = useScollegaAccount();
|
const scollega = useScollegaAccount();
|
||||||
const impostaAttivo = useImpostaAttivo();
|
const impostaAttivo = useImpostaAttivo();
|
||||||
|
|
||||||
const [datiSquadra, setDatiSquadra] = useState<DatiSquadra | null>(null);
|
const [datiSquadra, setDatiSquadra] = useState<DatiSquadra | null>(null);
|
||||||
const [bozza, setBozza] = useState<Profilo | null>(null);
|
const [bozza, setBozza] = useState<Profilo | null>(null);
|
||||||
const [tesseramento, setTesseramento] = useState<DatiTesseramento | null>(null);
|
|
||||||
|
|
||||||
const tesseramentoCorrente: DatiTesseramento = tesseramento ?? {
|
|
||||||
numeroTessera: g.numeroTessera,
|
|
||||||
dataTessera: g.dataTessera,
|
|
||||||
};
|
|
||||||
|
|
||||||
const squadraCorrente: DatiSquadra = datiSquadra ?? {
|
const squadraCorrente: DatiSquadra = datiSquadra ?? {
|
||||||
nome: g.nome,
|
nome: g.nome,
|
||||||
@@ -131,7 +121,7 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
|||||||
email: g.email,
|
email: g.email,
|
||||||
tipo: g.tipo,
|
tipo: g.tipo,
|
||||||
};
|
};
|
||||||
// L'allenatore non ha numero, ruolo in campo né tesseramento (DD-034).
|
// L'allenatore non ha numero né ruolo in campo (DD-034).
|
||||||
const allenatore = isAllenatore(g);
|
const allenatore = isAllenatore(g);
|
||||||
const profiloCorrente = bozza ?? profilo ?? profiloVuoto(g.id);
|
const profiloCorrente = bozza ?? profilo ?? profiloVuoto(g.id);
|
||||||
|
|
||||||
@@ -167,16 +157,6 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confermaTesseramento() {
|
|
||||||
try {
|
|
||||||
await salvaTesseramento.mutateAsync({ giocatoreId: g.id, dati: tesseramentoCorrente });
|
|
||||||
setTesseramento(null);
|
|
||||||
toast.success("Tesseramento aggiornato");
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(messaggioErrore(e, "Salvataggio non riuscito"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confermaProfilo() {
|
async function confermaProfilo() {
|
||||||
try {
|
try {
|
||||||
await salvaProfilo.mutateAsync(profiloCorrente);
|
await salvaProfilo.mutateAsync(profiloCorrente);
|
||||||
@@ -204,7 +184,7 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 space-y-3 border-t border-border pt-3">
|
<div className="mt-3 space-y-3 border-t border-border pt-3">
|
||||||
<h3 className="font-display text-sm uppercase tracking-wide">Dati squadra</h3>
|
<h3 className="font-display text-[15px] uppercase tracking-wide">Dati squadra</h3>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Campo label="Nome">
|
<Campo label="Nome">
|
||||||
<input
|
<input
|
||||||
@@ -264,54 +244,14 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={confermaSquadra}
|
onClick={confermaSquadra}
|
||||||
disabled={!datiSquadra || salvaSquadra.isPending}
|
disabled={!datiSquadra || salvaSquadra.isPending}
|
||||||
className="premi w-full rounded-2xl bg-primary py-2.5 text-xs font-bold uppercase text-primary-foreground disabled:opacity-50"
|
className="premi w-full rounded-2xl bg-primary py-2.5 text-[13px] font-bold uppercase text-primary-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Salva dati squadra
|
Salva dati squadra
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{allenatore ? null : (
|
|
||||||
<>
|
|
||||||
<h3 className="font-display text-sm uppercase tracking-wide">Tesseramento CSI</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<Campo label="Numero tessera">
|
|
||||||
<input
|
|
||||||
value={tesseramentoCorrente.numeroTessera ?? ""}
|
|
||||||
maxLength={40}
|
|
||||||
placeholder="Non ancora tesserato"
|
|
||||||
onChange={(e) =>
|
|
||||||
setTesseramento({
|
|
||||||
...tesseramentoCorrente,
|
|
||||||
numeroTessera: e.target.value || null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className={classiInput}
|
|
||||||
/>
|
|
||||||
</Campo>
|
|
||||||
<Campo label="Data tessera">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={tesseramentoCorrente.dataTessera ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setTesseramento({ ...tesseramentoCorrente, dataTessera: e.target.value || null })
|
|
||||||
}
|
|
||||||
className={classiInput}
|
|
||||||
/>
|
|
||||||
</Campo>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={confermaTesseramento}
|
|
||||||
disabled={!tesseramento || salvaTesseramento.isPending}
|
|
||||||
className="premi w-full rounded-2xl bg-primary py-2.5 text-xs font-bold uppercase text-primary-foreground disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Salva tesseramento
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allenatore ? (
|
{allenatore ? (
|
||||||
<>
|
<>
|
||||||
<h3 className="font-display text-sm uppercase tracking-wide">Dati personali</h3>
|
<h3 className="font-display text-[15px] uppercase tracking-wide">Dati personali</h3>
|
||||||
<CampiAllenatore
|
<CampiAllenatore
|
||||||
corrente={profiloCorrente}
|
corrente={profiloCorrente}
|
||||||
aggiorna={(patch) => setBozza({ ...profiloCorrente, ...patch })}
|
aggiorna={(patch) => setBozza({ ...profiloCorrente, ...patch })}
|
||||||
@@ -328,12 +268,12 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={confermaProfilo}
|
onClick={confermaProfilo}
|
||||||
disabled={!bozza || salvaProfilo.isPending}
|
disabled={!bozza || salvaProfilo.isPending}
|
||||||
className="premi w-full rounded-2xl bg-primary py-2.5 text-xs font-bold uppercase text-primary-foreground disabled:opacity-50"
|
className="premi w-full rounded-2xl bg-primary py-2.5 text-[13px] font-bold uppercase text-primary-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Salva dati personali
|
Salva dati personali
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3 border-t border-border pt-3 text-xs">
|
<div className="flex items-center justify-between gap-3 border-t border-border pt-3 text-[13px]">
|
||||||
<span className="min-w-0 text-muted-foreground">
|
<span className="min-w-0 text-muted-foreground">
|
||||||
{g.authUserId ? "Account collegato" : "Nessun account collegato"}
|
{g.authUserId ? "Account collegato" : "Nessun account collegato"}
|
||||||
</span>
|
</span>
|
||||||
@@ -404,7 +344,7 @@ function Documento({
|
|||||||
onClick={scarica}
|
onClick={scarica}
|
||||||
disabled={!path || inCorso}
|
disabled={!path || inCorso}
|
||||||
className={cn(
|
className={cn(
|
||||||
"premi flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase disabled:opacity-60",
|
"premi flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[13px] font-bold uppercase disabled:opacity-60",
|
||||||
statoClasse[stato],
|
statoClasse[stato],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -442,12 +382,12 @@ function SchedaGiocatore({
|
|||||||
return (
|
return (
|
||||||
<Reveal indice={indice} className="rounded-2xl bg-card p-4 shadow-card">
|
<Reveal indice={indice} className="rounded-2xl bg-card p-4 shadow-card">
|
||||||
<button type="button" onClick={onToggle} className="flex w-full items-center gap-3 text-left">
|
<button type="button" onClick={onToggle} className="flex w-full items-center gap-3 text-left">
|
||||||
<div className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-secondary font-display text-sm tabular-nums">
|
<div className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-secondary font-display text-[15px] tabular-nums">
|
||||||
{allenatore ? "All" : numero}
|
{allenatore ? "All" : numero}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate font-semibold leading-tight">{nome}</p>
|
<p className="truncate font-semibold leading-tight">{nome}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{ruoloVisibile(g)} · profilo {perc}%
|
{ruoloVisibile(g)} · profilo {perc}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -486,15 +426,6 @@ function SchedaGiocatore({
|
|||||||
stato={certificato}
|
stato={certificato}
|
||||||
path={profilo?.certificatoPath ?? null}
|
path={profilo?.certificatoPath ?? null}
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
|
|
||||||
statoClasse[g.numeroTessera ? "presente" : "assente"],
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<BadgeCheck className="h-3.5 w-3.5" />
|
|
||||||
{g.numeroTessera ? "Tesserato" : "Da tesserare"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -549,7 +480,7 @@ function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAperto(true)}
|
onClick={() => setAperto(true)}
|
||||||
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-secondary py-3 text-sm font-bold uppercase"
|
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase"
|
||||||
>
|
>
|
||||||
<UserPlus className="h-4 w-4" /> Aggiungi giocatore o allenatore
|
<UserPlus className="h-4 w-4" /> Aggiungi giocatore o allenatore
|
||||||
</button>
|
</button>
|
||||||
@@ -558,7 +489,7 @@ function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 space-y-3 rounded-2xl bg-card p-4 shadow-card">
|
<div className="mt-3 space-y-3 rounded-2xl bg-card p-4 shadow-card">
|
||||||
<h3 className="font-display text-sm uppercase tracking-wide">
|
<h3 className="font-display text-[15px] uppercase tracking-wide">
|
||||||
{allenatore ? "Nuovo allenatore" : "Nuovo giocatore"}
|
{allenatore ? "Nuovo allenatore" : "Nuovo giocatore"}
|
||||||
</h3>
|
</h3>
|
||||||
<Campo label="Tipo">
|
<Campo label="Tipo">
|
||||||
@@ -632,7 +563,7 @@ function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) {
|
|||||||
setAperto(false);
|
setAperto(false);
|
||||||
setDati(datiVuoti);
|
setDati(datiVuoti);
|
||||||
}}
|
}}
|
||||||
className="premi flex-1 rounded-2xl bg-secondary py-2.5 text-xs font-bold uppercase"
|
className="premi flex-1 rounded-2xl bg-secondary py-2.5 text-[13px] font-bold uppercase"
|
||||||
>
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</button>
|
</button>
|
||||||
@@ -640,7 +571,7 @@ function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void conferma()}
|
onClick={() => void conferma()}
|
||||||
disabled={aggiungi.isPending}
|
disabled={aggiungi.isPending}
|
||||||
className="premi flex-1 rounded-2xl bg-primary py-2.5 text-xs font-bold uppercase text-primary-foreground disabled:opacity-50"
|
className="premi flex-1 rounded-2xl bg-primary py-2.5 text-[13px] font-bold uppercase text-primary-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Aggiungi
|
Aggiungi
|
||||||
</button>
|
</button>
|
||||||
@@ -667,7 +598,7 @@ function GiocatoreDisattivato({ g }: { g: GiocatoreSquadra }) {
|
|||||||
<div className="flex items-center justify-between gap-3 rounded-2xl bg-card p-3 shadow-card">
|
<div className="flex items-center justify-between gap-3 rounded-2xl bg-card p-3 shadow-card">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate font-semibold leading-tight">{nomeCompleto(g)}</p>
|
<p className="truncate font-semibold leading-tight">{nomeCompleto(g)}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{isAllenatore(g) ? ruoloVisibile(g) : `#${g.numero} · ${g.ruolo}`}
|
{isAllenatore(g) ? ruoloVisibile(g) : `#${g.numero} · ${g.ruolo}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -675,7 +606,7 @@ function GiocatoreDisattivato({ g }: { g: GiocatoreSquadra }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void riattiva()}
|
onClick={() => void riattiva()}
|
||||||
disabled={impostaAttivo.isPending}
|
disabled={impostaAttivo.isPending}
|
||||||
className="premi flex shrink-0 items-center gap-1.5 rounded-xl bg-success px-3 py-2 text-xs font-bold text-success-foreground disabled:opacity-50"
|
className="premi flex shrink-0 items-center gap-1.5 rounded-xl bg-success px-3 py-2 text-[13px] font-bold text-success-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<UserCheck className="h-3.5 w-3.5" /> Riattiva
|
<UserCheck className="h-3.5 w-3.5" /> Riattiva
|
||||||
</button>
|
</button>
|
||||||
@@ -722,7 +653,7 @@ function Dashboard() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader titolo="Dashboard" sottotitolo="Area riservata" />
|
<PageHeader titolo="Dashboard" sottotitolo="Area riservata" />
|
||||||
<div className="px-5 pt-4">
|
<div className="px-5 pt-4">
|
||||||
<p className="flex items-center justify-center gap-2 rounded-3xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
<p className="flex items-center justify-center gap-2 rounded-3xl bg-card p-5 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
<Lock className="h-4 w-4 shrink-0" />
|
<Lock className="h-4 w-4 shrink-0" />
|
||||||
Riservata agli amministratori della squadra.
|
Riservata agli amministratori della squadra.
|
||||||
</p>
|
</p>
|
||||||
@@ -743,7 +674,6 @@ function Dashboard() {
|
|||||||
statoScadenza(profili[g.id]?.certificatoScadenza, profili[g.id]?.certificatoPath, oggi) ===
|
statoScadenza(profili[g.id]?.certificatoScadenza, profili[g.id]?.certificatoPath, oggi) ===
|
||||||
"valido",
|
"valido",
|
||||||
).length;
|
).length;
|
||||||
const tesserati = attivi.filter((g) => g.numeroTessera).length;
|
|
||||||
|
|
||||||
const contenutoSquadra = (
|
const contenutoSquadra = (
|
||||||
<>
|
<>
|
||||||
@@ -755,12 +685,11 @@ function Dashboard() {
|
|||||||
label="Certificati validi"
|
label="Certificati validi"
|
||||||
hint="non scaduti"
|
hint="non scaduti"
|
||||||
/>
|
/>
|
||||||
<StatTile valore={`${tesserati}/${attivi.length}`} label="Tesserati" hint="CSI" />
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => scaricaCsv(`tesseramento-csi-${oggi}.csv`, csvTesseramento(attivi, profili))}
|
onClick={() => scaricaCsv(`tesseramento-csi-${oggi}.csv`, csvTesseramento(attivi, profili))}
|
||||||
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" /> Esporta CSV tesseramento
|
<Download className="h-4 w-4" /> Esporta CSV tesseramento
|
||||||
</button>
|
</button>
|
||||||
@@ -769,7 +698,7 @@ function Dashboard() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const contenutoProfili = isPending ? (
|
const contenutoProfili = isPending ? (
|
||||||
<p className="rounded-2xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-2xl bg-card p-5 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
Caricamento…
|
Caricamento…
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -786,7 +715,7 @@ function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{allenatori.length > 0 ? (
|
{allenatori.length > 0 ? (
|
||||||
<h3 className="pt-2 font-display text-sm uppercase tracking-wide">Allenatori</h3>
|
<h3 className="pt-2 font-display text-[15px] uppercase tracking-wide">Allenatori</h3>
|
||||||
) : null}
|
) : null}
|
||||||
{allenatori.map((g, i) => (
|
{allenatori.map((g, i) => (
|
||||||
<SchedaGiocatore
|
<SchedaGiocatore
|
||||||
@@ -819,7 +748,7 @@ function Dashboard() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDestinatarioMessaggio({ nome: "tutta la squadra" })}
|
onClick={() => setDestinatarioMessaggio({ nome: "tutta la squadra" })}
|
||||||
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop"
|
||||||
>
|
>
|
||||||
<Send className="h-4 w-4" /> Invia messaggio a tutti
|
<Send className="h-4 w-4" /> Invia messaggio a tutti
|
||||||
</button>
|
</button>
|
||||||
@@ -835,7 +764,7 @@ function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 flex-1 truncate text-sm font-semibold leading-tight",
|
"min-w-0 flex-1 truncate text-[15px] font-semibold leading-tight",
|
||||||
!attiva && "text-muted-foreground",
|
!attiva && "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -855,7 +784,7 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-2xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-2xl bg-card p-5 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
Caricamento…
|
Caricamento…
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@@ -909,7 +838,7 @@ function Dashboard() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={inviaMessaggio}
|
onClick={inviaMessaggio}
|
||||||
disabled={inviaNotifica.isPending || !messaggio.trim()}
|
disabled={inviaNotifica.isPending || !messaggio.trim()}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{inviaNotifica.isPending ? (
|
{inviaNotifica.isPending ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
@@ -921,7 +850,7 @@ function Dashboard() {
|
|||||||
<DrawerClose asChild>
|
<DrawerClose asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-2xl bg-secondary py-3 text-sm font-bold uppercase text-foreground"
|
className="w-full rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase text-foreground"
|
||||||
>
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ function AllenamentoDetail() {
|
|||||||
if (!evento) {
|
if (!evento) {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 pt-8">
|
<div className="px-5 pt-8">
|
||||||
<p className="mt-8 text-center text-sm text-muted-foreground">Allenamento non trovato</p>
|
<p className="mt-8 text-center text-[15px] text-muted-foreground">
|
||||||
|
Allenamento non trovato
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,14 +61,14 @@ function AllenamentoDetail() {
|
|||||||
<Dumbbell className="h-6 w-6" />
|
<Dumbbell className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Allenamento di squadra
|
Allenamento di squadra
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-lg font-bold leading-tight">{evento.titolo}</p>
|
<p className="truncate text-lg font-bold leading-tight">{evento.titolo}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 space-y-2 text-sm text-muted-foreground">
|
<div className="mt-4 space-y-2 text-[15px] text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4" /> {evento.ora}
|
<Clock className="h-4 w-4" /> {evento.ora}
|
||||||
</span>
|
</span>
|
||||||
@@ -76,12 +78,12 @@ function AllenamentoDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{evento.note ? (
|
{evento.note ? (
|
||||||
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-sm">
|
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-[15px]">
|
||||||
{evento.note}
|
{evento.note}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1.5 text-xs font-semibold">
|
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1.5 text-[13px] font-semibold">
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4" />
|
||||||
Conferme: {presentiVeri}/{convocatiEvento(evento, rosa).length}
|
Conferme: {presentiVeri}/{convocatiEvento(evento, rosa).length}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -107,34 +107,34 @@ function Benvenuto() {
|
|||||||
|
|
||||||
{!pronta ? null : !utenteId ? (
|
{!pronta ? null : !utenteId ? (
|
||||||
<>
|
<>
|
||||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
<p className="mt-2 text-center text-[15px] text-muted-foreground">
|
||||||
Accedi con il tuo account Google per collegare il profilo giocatore.
|
Accedi con il tuo account Google per collegare il profilo giocatore.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={accedi}
|
onClick={accedi}
|
||||||
disabled={inCorso}
|
disabled={inCorso}
|
||||||
className="premi mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3.5 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-60"
|
className="premi mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3.5 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<LogIn className="h-4 w-4" /> Accedi con Google
|
<LogIn className="h-4 w-4" /> Accedi con Google
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : erroreCollegamento ? (
|
) : erroreCollegamento ? (
|
||||||
<>
|
<>
|
||||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
<p className="mt-2 text-center text-[15px] text-muted-foreground">
|
||||||
Nessun profilo trovato per la tua email. Contatta un amministratore per collegare il tuo
|
Nessun profilo trovato per la tua email. Contatta un amministratore per collegare il tuo
|
||||||
account.
|
account.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void esciERiprova()}
|
onClick={() => void esciERiprova()}
|
||||||
className="premi mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-secondary py-3.5 text-sm font-bold uppercase shadow-card"
|
className="premi mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-secondary py-3.5 text-[15px] font-bold uppercase shadow-card"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" /> Esci
|
<LogOut className="h-4 w-4" /> Esci
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : inAttesaCollegamento ? (
|
) : inAttesaCollegamento ? (
|
||||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
<p className="mt-2 text-center text-[15px] text-muted-foreground">
|
||||||
Ti stiamo collegando al tuo profilo...
|
Ti stiamo collegando al tuo profilo...
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ function Calendario() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-1 text-center text-xs font-bold text-muted-foreground">
|
<div className="grid grid-cols-7 gap-1 text-center text-[13px] font-bold text-muted-foreground">
|
||||||
{giorniIT.map((g, i) => (
|
{giorniIT.map((g, i) => (
|
||||||
<span key={i}>{g}</span>
|
<span key={i}>{g}</span>
|
||||||
))}
|
))}
|
||||||
@@ -192,7 +192,7 @@ function Calendario() {
|
|||||||
onClick={haEventi ? () => apriGiorno(giorno) : undefined}
|
onClick={haEventi ? () => apriGiorno(giorno) : undefined}
|
||||||
style={sfondo ? { backgroundImage: sfondo } : undefined}
|
style={sfondo ? { backgroundImage: sfondo } : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative grid aspect-square place-items-center rounded-xl text-sm font-semibold",
|
"relative grid aspect-square place-items-center rounded-xl text-[15px] font-semibold",
|
||||||
tipo === "partita" && "bg-accent text-accent-foreground",
|
tipo === "partita" && "bg-accent text-accent-foreground",
|
||||||
tipo === "allenamento" && "bg-training text-training-foreground",
|
tipo === "allenamento" && "bg-training text-training-foreground",
|
||||||
tipo === "evento" && "bg-warning text-warning-foreground",
|
tipo === "evento" && "bg-warning text-warning-foreground",
|
||||||
@@ -224,10 +224,10 @@ function Calendario() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
Scorri a destra o sinistra per cambiare mese.
|
Scorri a destra o sinistra per cambiare mese.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3 flex flex-wrap gap-3 text-xs font-semibold text-muted-foreground">
|
<div className="mt-3 flex flex-wrap gap-3 text-[13px] font-semibold text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita
|
<i className="h-2.5 w-2.5 rounded-full bg-accent" /> Partita
|
||||||
</span>
|
</span>
|
||||||
@@ -248,7 +248,7 @@ function Calendario() {
|
|||||||
<div className="px-5 pt-4">
|
<div className="px-5 pt-4">
|
||||||
<Link
|
<Link
|
||||||
to="/eventi"
|
to="/eventi"
|
||||||
className="premi flex items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[14px] font-bold uppercase text-accent-foreground shadow-pop"
|
className="premi flex items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop"
|
||||||
>
|
>
|
||||||
<CalendarPlus className="h-4 w-4" /> Gestisci eventi
|
<CalendarPlus className="h-4 w-4" /> Gestisci eventi
|
||||||
</Link>
|
</Link>
|
||||||
@@ -263,7 +263,7 @@ function Calendario() {
|
|||||||
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
|
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-3xl bg-card p-4 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-4 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
Nessun evento in programma
|
Nessun evento in programma
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -275,7 +275,7 @@ function Calendario() {
|
|||||||
{compleanniMese.length > 0 ? (
|
{compleanniMese.length > 0 ? (
|
||||||
compleanniMese.map((c) => <EventoCard key={c.id} evento={c} />)
|
compleanniMese.map((c) => <EventoCard key={c.id} evento={c} />)
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-3xl bg-card p-4 text-center text-sm 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()}
|
Nessun compleanno in {mesiIT[mese]!.toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -300,7 +300,7 @@ function Calendario() {
|
|||||||
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
|
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-3xl bg-card p-4 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-4 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
Nessun evento in questa data
|
Nessun evento in questa data
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+15
-15
@@ -82,7 +82,7 @@ export const Route = createFileRoute("/classifica")({
|
|||||||
function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: string }) {
|
function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-3xl bg-card shadow-card">
|
<div className="overflow-hidden rounded-3xl bg-card shadow-card">
|
||||||
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-xs font-bold uppercase text-muted-foreground">
|
<div className="grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] gap-2 border-b border-border px-3 py-2 text-[13px] font-bold uppercase text-muted-foreground">
|
||||||
<span>#</span>
|
<span>#</span>
|
||||||
<span>Squadra</span>
|
<span>Squadra</span>
|
||||||
<span className="text-center">G</span>
|
<span className="text-center">G</span>
|
||||||
@@ -90,7 +90,7 @@ function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: s
|
|||||||
<span className="text-center">Pt</span>
|
<span className="text-center">Pt</span>
|
||||||
</div>
|
</div>
|
||||||
{righe.length === 0 ? (
|
{righe.length === 0 ? (
|
||||||
<p className="px-3 py-4 text-center text-xs text-muted-foreground">{vuoto}</p>
|
<p className="px-3 py-4 text-center text-[13px] text-muted-foreground">{vuoto}</p>
|
||||||
) : (
|
) : (
|
||||||
righe.map((r) => {
|
righe.map((r) => {
|
||||||
const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley";
|
const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley";
|
||||||
@@ -98,14 +98,14 @@ function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: s
|
|||||||
<div
|
<div
|
||||||
key={r.pos}
|
key={r.pos}
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] items-center gap-2 border-b border-border px-3 py-2.5 text-sm last:border-0",
|
"grid grid-cols-[2rem_minmax(0,1fr)_2rem_2.5rem_2.5rem] items-center gap-2 border-b border-border px-3 py-2.5 text-[15px] last:border-0",
|
||||||
noi && "bg-accent/10",
|
noi && "bg-accent/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={cn("font-display text-base", noi && "text-accent")}>{r.pos}</span>
|
<span className={cn("font-display text-base", noi && "text-accent")}>{r.pos}</span>
|
||||||
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>{r.squadra}</span>
|
<span className={cn("truncate", noi ? "font-bold" : "font-medium")}>{r.squadra}</span>
|
||||||
<span className="text-center text-xs text-muted-foreground">{r.giocate}</span>
|
<span className="text-center text-[13px] text-muted-foreground">{r.giocate}</span>
|
||||||
<span className="text-center text-xs tabular-nums text-muted-foreground">
|
<span className="text-center text-[13px] tabular-nums text-muted-foreground">
|
||||||
{r.setFatti}:{r.setSubiti}
|
{r.setFatti}:{r.setSubiti}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-center font-bold tabular-nums">{r.punti}</span>
|
<span className="text-center font-bold tabular-nums">{r.punti}</span>
|
||||||
@@ -171,7 +171,7 @@ function Classifica() {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-3 flex items-center gap-2 rounded-2xl px-3 py-2 text-xs",
|
"mb-3 flex items-center gap-2 rounded-2xl px-3 py-2 text-[13px]",
|
||||||
csi?.formatoSospetto
|
csi?.formatoSospetto
|
||||||
? "bg-warning/15 text-warning"
|
? "bg-warning/15 text-warning"
|
||||||
: "bg-secondary text-muted-foreground",
|
: "bg-secondary text-muted-foreground",
|
||||||
@@ -190,9 +190,9 @@ function Classifica() {
|
|||||||
</div>
|
</div>
|
||||||
{classificaCoppa.length > 0 ? (
|
{classificaCoppa.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-2 px-1 text-sm font-bold text-foreground">Coppa</h3>
|
<h3 className="mb-2 px-1 text-[15px] font-bold text-foreground">Coppa</h3>
|
||||||
<TabellaClassifica righe={classificaCoppa} vuoto="Classifica non disponibile." />
|
<TabellaClassifica righe={classificaCoppa} vuoto="Classifica non disponibile." />
|
||||||
<h3 className="mb-2 mt-4 px-1 text-sm font-bold text-foreground">Girone</h3>
|
<h3 className="mb-2 mt-4 px-1 text-[15px] font-bold text-foreground">Girone</h3>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<TabellaClassifica righe={classifica} vuoto="Classifica non ancora disponibile." />
|
<TabellaClassifica righe={classifica} vuoto="Classifica non ancora disponibile." />
|
||||||
@@ -204,14 +204,14 @@ function Classifica() {
|
|||||||
const contenutoStorico = useMemo(
|
const contenutoStorico = useMemo(
|
||||||
() =>
|
() =>
|
||||||
tuttiMatch.length === 0 ? (
|
tuttiMatch.length === 0 ? (
|
||||||
<p className="rounded-3xl bg-card p-4 text-center text-xs text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-4 text-center text-[13px] text-muted-foreground shadow-card">
|
||||||
Nessuna partita disponibile.
|
Nessuna partita disponibile.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{raggruppaPerStagione(tuttiMatch).map(({ stagione, match }) => (
|
{raggruppaPerStagione(tuttiMatch).map(({ stagione, match }) => (
|
||||||
<section key={stagione} className="space-y-3">
|
<section key={stagione} className="space-y-3">
|
||||||
<h3 className="px-1 text-sm font-bold text-foreground">Stagione {stagione}</h3>
|
<h3 className="px-1 text-[15px] font-bold text-foreground">Stagione {stagione}</h3>
|
||||||
{match.map((m) => {
|
{match.map((m) => {
|
||||||
const vinta = m.setNostri > m.setLoro;
|
const vinta = m.setNostri > m.setLoro;
|
||||||
const eventoId = eventoIdPerData.get(m.data);
|
const eventoId = eventoIdPerData.get(m.data);
|
||||||
@@ -237,7 +237,7 @@ function Classifica() {
|
|||||||
logoAvversario={m.logoAvversario}
|
logoAvversario={m.logoAvversario}
|
||||||
avversario={m.avversario}
|
avversario={m.avversario}
|
||||||
/>
|
/>
|
||||||
<p className="truncate text-sm font-bold">{casa.nome}</p>
|
<p className="truncate text-[15px] font-bold">{casa.nome}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<LogoPartita
|
<LogoPartita
|
||||||
@@ -245,9 +245,9 @@ function Classifica() {
|
|||||||
logoAvversario={m.logoAvversario}
|
logoAvversario={m.logoAvversario}
|
||||||
avversario={m.avversario}
|
avversario={m.avversario}
|
||||||
/>
|
/>
|
||||||
<p className="truncate text-sm font-bold">{trasferta.nome}</p>
|
<p className="truncate text-[15px] font-bold">{trasferta.nome}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{formatData(m.data)} · MVP {m.mvp || "da votare"}
|
{formatData(m.data)} · MVP {m.mvp || "da votare"}
|
||||||
{m.scout ? " · scoutata" : ""}
|
{m.scout ? " · scoutata" : ""}
|
||||||
</p>
|
</p>
|
||||||
@@ -278,7 +278,7 @@ function Classifica() {
|
|||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg px-2 py-1 text-xs font-semibold tabular-nums",
|
"rounded-lg px-2 py-1 text-[13px] font-semibold tabular-nums",
|
||||||
setVintoDaNoi ? "bg-secondary" : "bg-muted text-muted-foreground",
|
setVintoDaNoi ? "bg-secondary" : "bg-muted text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -287,7 +287,7 @@ function Classifica() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{eventoId && !m.mvp ? (
|
{eventoId && !m.mvp ? (
|
||||||
<span className="ml-auto inline-flex items-center gap-0.5 text-xs font-bold uppercase text-accent">
|
<span className="ml-auto inline-flex items-center gap-0.5 text-[13px] font-bold uppercase text-accent">
|
||||||
Vota MVP <ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
Vota MVP <ChevronRight className="h-3.5 w-3.5" aria-hidden />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
+26
-24
@@ -91,7 +91,7 @@ function GestioneEventi() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader titolo="Gestione eventi" sottotitolo="Area riservata" />
|
<PageHeader titolo="Gestione eventi" sottotitolo="Area riservata" />
|
||||||
<div className="px-5">
|
<div className="px-5">
|
||||||
<p className="rounded-3xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-5 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
Solo i referenti della squadra possono creare o modificare gli eventi.
|
Solo i referenti della squadra possono creare o modificare gli eventi.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +207,7 @@ function GestioneEventi() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-1 text-center text-xs font-bold text-muted-foreground">
|
<div className="grid grid-cols-7 gap-1 text-center text-[13px] font-bold text-muted-foreground">
|
||||||
{giorniIT.map((g, i) => (
|
{giorniIT.map((g, i) => (
|
||||||
<span key={i}>{g}</span>
|
<span key={i}>{g}</span>
|
||||||
))}
|
))}
|
||||||
@@ -248,7 +248,7 @@ function GestioneEventi() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => apriGiorno(giorno)}
|
onClick={() => apriGiorno(giorno)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative grid aspect-square place-items-center rounded-xl text-sm font-semibold transition-transform active:scale-90",
|
"relative grid aspect-square place-items-center rounded-xl text-[15px] font-semibold transition-transform active:scale-90",
|
||||||
haEventi
|
haEventi
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "bg-secondary text-foreground",
|
: "bg-secondary text-foreground",
|
||||||
@@ -265,11 +265,11 @@ function GestioneEventi() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<p aria-busy="true" className="mt-2 text-xs text-muted-foreground">
|
<p aria-busy="true" className="mt-2 text-[13px] text-muted-foreground">
|
||||||
Carico gli eventi…
|
Carico gli eventi…
|
||||||
</p>
|
</p>
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<div className="mt-3 space-y-2 text-center text-sm">
|
<div className="mt-3 space-y-2 text-center text-[15px]">
|
||||||
<p className="text-destructive">
|
<p className="text-destructive">
|
||||||
Non sono riuscito a caricare gli eventi
|
Non sono riuscito a caricare gli eventi
|
||||||
{error instanceof Error ? `: ${error.message}` : ""}.
|
{error instanceof Error ? `: ${error.message}` : ""}.
|
||||||
@@ -277,13 +277,13 @@ function GestioneEventi() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => refetch()}
|
onClick={() => refetch()}
|
||||||
className="rounded-2xl bg-secondary px-4 py-2 text-xs font-bold uppercase"
|
className="rounded-2xl bg-secondary px-4 py-2 text-[13px] font-bold uppercase"
|
||||||
>
|
>
|
||||||
Riprova
|
Riprova
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
Scorri a destra o sinistra per cambiare mese. Tocca un giorno per aggiungere,
|
Scorri a destra o sinistra per cambiare mese. Tocca un giorno per aggiungere,
|
||||||
modificare o eliminare.
|
modificare o eliminare.
|
||||||
</p>
|
</p>
|
||||||
@@ -303,7 +303,7 @@ function GestioneEventi() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => aggiorna(daCategoria(t.id))}
|
onClick={() => aggiorna(daCategoria(t.id))}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full py-2 text-xs font-bold uppercase transition-colors",
|
"rounded-full py-2 text-[13px] font-bold uppercase transition-colors",
|
||||||
categoriaEvento(bozza) === t.id
|
categoriaEvento(bozza) === t.id
|
||||||
? "bg-card shadow-card text-foreground"
|
? "bg-card shadow-card text-foreground"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
@@ -337,15 +337,17 @@ function GestioneEventi() {
|
|||||||
</Campo>
|
</Campo>
|
||||||
) : (
|
) : (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<span className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Giorno
|
Giorno
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-1 flex h-10 items-center justify-between gap-1">
|
<div className="mt-1 flex h-10 items-center justify-between gap-1">
|
||||||
<span className="truncate text-sm font-semibold">{formatData(bozza.data)}</span>
|
<span className="truncate text-[15px] font-semibold">
|
||||||
|
{formatData(bozza.data)}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCambiaGiorno(true)}
|
onClick={() => setCambiaGiorno(true)}
|
||||||
className="shrink-0 rounded-full px-2 py-1 text-xs font-bold uppercase text-accent active:scale-95"
|
className="shrink-0 rounded-full px-2 py-1 text-[13px] font-bold uppercase text-accent active:scale-95"
|
||||||
>
|
>
|
||||||
Cambia
|
Cambia
|
||||||
</button>
|
</button>
|
||||||
@@ -394,7 +396,7 @@ function GestioneEventi() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => aggiorna({ casa: o.casa })}
|
onClick={() => aggiorna({ casa: o.casa })}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 rounded-full py-2 text-xs font-bold uppercase transition-colors",
|
"flex-1 rounded-full py-2 text-[13px] font-bold uppercase transition-colors",
|
||||||
bozza.casa === o.casa
|
bozza.casa === o.casa
|
||||||
? "bg-card shadow-card text-foreground"
|
? "bg-card shadow-card text-foreground"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
@@ -433,7 +435,7 @@ function GestioneEventi() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"truncate rounded-xl px-2.5 py-2 text-left text-xs font-semibold transition-colors",
|
"truncate rounded-xl px-2.5 py-2 text-left text-[13px] font-semibold transition-colors",
|
||||||
scelto
|
scelto
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "bg-secondary text-muted-foreground",
|
: "bg-secondary text-muted-foreground",
|
||||||
@@ -450,7 +452,7 @@ function GestioneEventi() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setBozza(null)}
|
onClick={() => setBozza(null)}
|
||||||
className="flex-1 rounded-2xl bg-secondary py-3 text-sm font-bold uppercase"
|
className="flex-1 rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase"
|
||||||
>
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</button>
|
</button>
|
||||||
@@ -458,7 +460,7 @@ function GestioneEventi() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={chiediConferma}
|
onClick={chiediConferma}
|
||||||
disabled={salva.isPending}
|
disabled={salva.isPending}
|
||||||
className="flex flex-1 items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
className="flex flex-1 items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null} Salva
|
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null} Salva
|
||||||
</button>
|
</button>
|
||||||
@@ -489,8 +491,8 @@ function GestioneEventi() {
|
|||||||
className="flex items-center gap-2 rounded-2xl bg-card p-3 shadow-card"
|
className="flex items-center gap-2 rounded-2xl bg-card p-3 shadow-card"
|
||||||
>
|
>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-bold leading-tight">{e.titolo}</p>
|
<p className="truncate text-[15px] font-bold leading-tight">{e.titolo}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{e.ora} · {e.luogo || "luogo da definire"}
|
{e.ora} · {e.luogo || "luogo da definire"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -513,7 +515,7 @@ function GestioneEventi() {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-2xl bg-card p-4 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-2xl bg-card p-4 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
Nessun evento in programma
|
Nessun evento in programma
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -521,7 +523,7 @@ function GestioneEventi() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={nuovoNelGiorno}
|
onClick={nuovoNelGiorno}
|
||||||
className="premi mt-1 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
className="premi mt-1 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop"
|
||||||
>
|
>
|
||||||
<CalendarPlus className="h-4 w-4" /> Nuovo evento in questo giorno
|
<CalendarPlus className="h-4 w-4" /> Nuovo evento in questo giorno
|
||||||
</button>
|
</button>
|
||||||
@@ -544,14 +546,14 @@ function GestioneEventi() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={conferma}
|
onClick={conferma}
|
||||||
disabled={salva.isPending}
|
disabled={salva.isPending}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null} Conferma
|
{salva.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null} Conferma
|
||||||
</button>
|
</button>
|
||||||
<DrawerClose asChild>
|
<DrawerClose asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-2xl bg-secondary py-3 text-sm font-bold uppercase text-foreground"
|
className="w-full rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase text-foreground"
|
||||||
>
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</button>
|
</button>
|
||||||
@@ -573,14 +575,14 @@ function GestioneEventi() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => daEliminare && rimuovi(daEliminare.id)}
|
onClick={() => daEliminare && rimuovi(daEliminare.id)}
|
||||||
disabled={elimina.isPending}
|
disabled={elimina.isPending}
|
||||||
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-destructive py-3 text-sm font-bold uppercase text-destructive-foreground disabled:opacity-50"
|
className="flex w-full items-center justify-center gap-2 rounded-2xl bg-destructive py-3 text-[15px] font-bold uppercase text-destructive-foreground disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{elimina.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null} Elimina
|
{elimina.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : null} Elimina
|
||||||
</button>
|
</button>
|
||||||
<DrawerClose asChild>
|
<DrawerClose asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full rounded-2xl bg-secondary py-3 text-sm font-bold uppercase text-foreground"
|
className="w-full rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase text-foreground"
|
||||||
>
|
>
|
||||||
Annulla
|
Annulla
|
||||||
</button>
|
</button>
|
||||||
@@ -606,7 +608,7 @@ function Interruttore({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1.5 text-xs font-bold uppercase transition-colors",
|
"rounded-full px-3 py-1.5 text-[13px] font-bold uppercase transition-colors",
|
||||||
attivo ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground",
|
attivo ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function EventoDetail() {
|
|||||||
if (!evento) {
|
if (!evento) {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 pt-8">
|
<div className="px-5 pt-8">
|
||||||
<p className="mt-8 text-center text-sm text-muted-foreground">Evento non trovato</p>
|
<p className="mt-8 text-center text-[15px] text-muted-foreground">Evento non trovato</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -58,14 +58,14 @@ function EventoDetail() {
|
|||||||
<PartyPopper className="h-6 w-6" />
|
<PartyPopper className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Evento extra-campo
|
Evento extra-campo
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-lg font-bold leading-tight">{evento.titolo}</p>
|
<p className="truncate text-lg font-bold leading-tight">{evento.titolo}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 space-y-2 text-sm text-muted-foreground">
|
<div className="mt-4 space-y-2 text-[15px] text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4" /> {evento.ora}
|
<Clock className="h-4 w-4" /> {evento.ora}
|
||||||
</span>
|
</span>
|
||||||
@@ -75,12 +75,12 @@ function EventoDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{evento.note ? (
|
{evento.note ? (
|
||||||
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-sm">
|
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-[15px]">
|
||||||
{evento.note}
|
{evento.note}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1.5 text-xs font-semibold">
|
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1.5 text-[13px] font-semibold">
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4" />
|
||||||
Conferme: {presentiVeri}/{convocatiEvento(evento, rosa).length}
|
Conferme: {presentiVeri}/{convocatiEvento(evento, rosa).length}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+42
-27
@@ -9,6 +9,7 @@ import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
|||||||
import { Reveal } from "@/components/motion/Reveal";
|
import { Reveal } from "@/components/motion/Reveal";
|
||||||
import { Barra } from "@/components/motion/Barra";
|
import { Barra } from "@/components/motion/Barra";
|
||||||
import { Numero } from "@/components/motion/Numero";
|
import { Numero } from "@/components/motion/Numero";
|
||||||
|
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
|
||||||
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
||||||
import { useEventi, type Evento } from "@/lib/eventi";
|
import { useEventi, type Evento } from "@/lib/eventi";
|
||||||
import { useRispostePresenze } from "@/lib/presenze";
|
import { useRispostePresenze } from "@/lib/presenze";
|
||||||
@@ -85,7 +86,7 @@ function Index() {
|
|||||||
className="h-14 w-14 rounded-full shadow-pop ring-2 ring-primary-foreground/25"
|
className="h-14 w-14 rounded-full shadow-pop ring-2 ring-primary-foreground/25"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary-foreground/80">
|
<p className="text-[13px] font-semibold uppercase tracking-[0.2em] text-primary-foreground/80">
|
||||||
Ciao {nome.split(" ")[0]}
|
Ciao {nome.split(" ")[0]}
|
||||||
</p>
|
</p>
|
||||||
<h1 className="font-display-lg text-4xl uppercase leading-none">CRAP Volley</h1>
|
<h1 className="font-display-lg text-4xl uppercase leading-none">CRAP Volley</h1>
|
||||||
@@ -99,13 +100,13 @@ function Index() {
|
|||||||
>
|
>
|
||||||
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
||||||
<p className="font-display text-2xl leading-none">{noi ? `${noi.pos}º` : "—"}</p>
|
<p className="font-display text-2xl leading-none">{noi ? `${noi.pos}º` : "—"}</p>
|
||||||
<p className="text-xs uppercase text-primary-foreground/80">In classifica</p>
|
<p className="text-[13px] uppercase text-primary-foreground/80">In classifica</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
||||||
<p className="font-display text-2xl leading-none">
|
<p className="font-display text-2xl leading-none">
|
||||||
{noi ? `${noi.vinte}-${noi.perse}` : "—"}
|
{noi ? `${noi.vinte}-${noi.perse}` : "—"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs uppercase text-primary-foreground/80">Bilancio W-L</p>
|
<p className="text-[13px] uppercase text-primary-foreground/80">Bilancio W-L</p>
|
||||||
</div>
|
</div>
|
||||||
{giocatore ? (
|
{giocatore ? (
|
||||||
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
<div className="rounded-2xl bg-primary-foreground/10 p-3">
|
||||||
@@ -113,7 +114,7 @@ function Index() {
|
|||||||
<Flame className="h-4 w-4 text-accent" />
|
<Flame className="h-4 w-4 text-accent" />
|
||||||
{giocatore.streak}
|
{giocatore.streak}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs uppercase text-primary-foreground/80">Streak</p>
|
<p className="text-[13px] uppercase text-primary-foreground/80">Streak</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +132,7 @@ function Index() {
|
|||||||
azione={
|
azione={
|
||||||
<Link
|
<Link
|
||||||
to="/calendario"
|
to="/calendario"
|
||||||
className="inline-flex items-center text-[14px] font-semibold text-accent"
|
className="inline-flex items-center text-[15px] font-semibold text-accent"
|
||||||
>
|
>
|
||||||
Calendario <ChevronRight className="h-4 w-4" />
|
Calendario <ChevronRight className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -140,7 +141,7 @@ function Index() {
|
|||||||
{prossimo ? (
|
{prossimo ? (
|
||||||
<EventoCard evento={prossimo} {...(linkProssimo ? { linkTo: linkProssimo } : {})} />
|
<EventoCard evento={prossimo} {...(linkProssimo ? { linkTo: linkProssimo } : {})} />
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-3xl bg-card p-4 text-xs text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-4 text-[13px] text-muted-foreground shadow-card">
|
||||||
Nessun impegno in programma.
|
Nessun impegno in programma.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -155,7 +156,7 @@ function Index() {
|
|||||||
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
|
return <EventoCard key={e.id} evento={e} {...(link ? { linkTo: link } : {})} />;
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-3xl bg-card p-4 text-xs text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-4 text-[13px] text-muted-foreground shadow-card">
|
||||||
Nient'altro da confermare: sei in pari.
|
Nient'altro da confermare: sei in pari.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -170,7 +171,7 @@ function Index() {
|
|||||||
<Link
|
<Link
|
||||||
to="/classifica"
|
to="/classifica"
|
||||||
search={{ tab: "storico" }}
|
search={{ tab: "storico" }}
|
||||||
className="inline-flex items-center text-[14px] font-semibold text-accent"
|
className="inline-flex items-center text-[15px] font-semibold text-accent"
|
||||||
>
|
>
|
||||||
Storico <ChevronRight className="h-4 w-4" />
|
Storico <ChevronRight className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -182,8 +183,10 @@ function Index() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-bold">CRAP Volley vs {ultima.avversario}</p>
|
<p className="truncate text-[15px] font-bold">
|
||||||
<p className="text-xs text-muted-foreground">
|
CRAP Volley vs {ultima.avversario}
|
||||||
|
</p>
|
||||||
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{ultima.casa ? "In casa" : "Trasferta"}
|
{ultima.casa ? "In casa" : "Trasferta"}
|
||||||
{ultima.mvp ? ` · MVP ${ultima.mvp}` : " · MVP da votare"}
|
{ultima.mvp ? ` · MVP ${ultima.mvp}` : " · MVP da votare"}
|
||||||
</p>
|
</p>
|
||||||
@@ -196,7 +199,7 @@ function Index() {
|
|||||||
{ultima.parziali.map((p, i) => (
|
{ultima.parziali.map((p, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className="rounded-lg bg-secondary px-2 py-1 text-xs font-semibold tabular-nums"
|
className="rounded-lg bg-secondary px-2 py-1 text-[13px] font-semibold tabular-nums"
|
||||||
>
|
>
|
||||||
{p[0]}-{p[1]}
|
{p[0]}-{p[1]}
|
||||||
</span>
|
</span>
|
||||||
@@ -217,7 +220,7 @@ function Index() {
|
|||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
) : (
|
) : (
|
||||||
<p className="rounded-3xl bg-card p-4 text-xs text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-4 text-[13px] text-muted-foreground shadow-card">
|
||||||
Nessun risultato disponibile.
|
Nessun risultato disponibile.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -230,27 +233,39 @@ function Index() {
|
|||||||
azione={
|
azione={
|
||||||
<Link
|
<Link
|
||||||
to="/squadra"
|
to="/squadra"
|
||||||
className="inline-flex items-center text-[14px] font-semibold text-accent"
|
className="inline-flex items-center text-[15px] font-semibold text-accent"
|
||||||
>
|
>
|
||||||
Tutti <ChevronRight className="h-4 w-4" />
|
Tutti <ChevronRight className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{obiettivo ? (
|
{obiettivo ? (
|
||||||
<Card>
|
<ObiettivoDrawer obiettivo={obiettivo}>
|
||||||
<div className="flex items-center gap-2 text-sm font-bold">
|
<button
|
||||||
<span className="text-base leading-none">{obiettivo.emoji}</span> {obiettivo.titolo}
|
type="button"
|
||||||
</div>
|
aria-label={`Dettaglio obiettivo: ${obiettivo.titolo}`}
|
||||||
<Barra percentuale={progressoObiettivo(obiettivo)} trackClassName="mt-3" />
|
className="block w-full text-left"
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
>
|
||||||
Siamo al <Numero valore={progressoObiettivo(obiettivo)} suffisso="%" /> —{" "}
|
<Card>
|
||||||
{obiettivo.valore}/{obiettivo.target} {obiettivo.unita}.
|
<div className="flex items-center gap-2 text-[15px] font-bold">
|
||||||
</p>
|
<span className="text-base leading-none">{obiettivo.emoji}</span>{" "}
|
||||||
<p className="mt-1 text-xs font-semibold text-accent">
|
{obiettivo.titolo}
|
||||||
{microcopyObiettivo(obiettivo)}
|
</div>
|
||||||
</p>
|
<Barra percentuale={progressoObiettivo(obiettivo)} trackClassName="mt-3" />
|
||||||
<p className="mt-1 text-xs text-muted-foreground">{obiettivo.impatto}</p>
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
</Card>
|
Siamo al <Numero valore={progressoObiettivo(obiettivo)} suffisso="%" /> —{" "}
|
||||||
|
{obiettivo.valore}/{obiettivo.target} {obiettivo.unita}.
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[13px] font-semibold text-accent">
|
||||||
|
{microcopyObiettivo(obiettivo)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[13px] text-muted-foreground">{obiettivo.impatto}</p>
|
||||||
|
<p className="mt-2 inline-flex items-center text-[13px] font-semibold text-accent">
|
||||||
|
Come funziona <ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
</button>
|
||||||
|
</ObiettivoDrawer>
|
||||||
) : null}
|
) : null}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function PartitaCsiDetail() {
|
|||||||
if (!partita) {
|
if (!partita) {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 pt-8">
|
<div className="px-5 pt-8">
|
||||||
<p className="mt-8 text-center text-sm text-muted-foreground">Partita non trovata</p>
|
<p className="mt-8 text-center text-[15px] text-muted-foreground">Partita non trovata</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ function PartitaCsiDetail() {
|
|||||||
className="h-10 w-10"
|
className="h-10 w-10"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
{partita.casa ? "In casa" : "Fuori casa"}
|
{partita.casa ? "In casa" : "Fuori casa"}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-lg font-bold leading-tight">{partita.avversario}</p>
|
<p className="truncate text-lg font-bold leading-tight">{partita.avversario}</p>
|
||||||
@@ -93,7 +93,7 @@ function PartitaCsiDetail() {
|
|||||||
{partita.parziali.map(([noi, loro], i) => (
|
{partita.parziali.map(([noi, loro], i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className="rounded-lg bg-secondary px-2 py-1 text-xs font-semibold tabular-nums"
|
className="rounded-lg bg-secondary px-2 py-1 text-[13px] font-semibold tabular-nums"
|
||||||
>
|
>
|
||||||
{noi}-{loro}
|
{noi}-{loro}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+15
-13
@@ -60,7 +60,7 @@ function PartitaDetail() {
|
|||||||
if (!evento) {
|
if (!evento) {
|
||||||
return (
|
return (
|
||||||
<div className="px-5 pt-8">
|
<div className="px-5 pt-8">
|
||||||
<p className="mt-8 text-center text-sm text-muted-foreground">Partita non trovata</p>
|
<p className="mt-8 text-center text-[15px] text-muted-foreground">Partita non trovata</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ function PartitaDetail() {
|
|||||||
<LogoSquadra src={match.logoAvversario} alt={avversario} className="h-10 w-10" />
|
<LogoSquadra src={match.logoAvversario} alt={avversario} className="h-10 w-10" />
|
||||||
) : null}
|
) : null}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
{casa ? "In casa" : "Fuori casa"}
|
{casa ? "In casa" : "Fuori casa"}
|
||||||
{evento.campionato ? " · Campionato" : " · Amichevole"}
|
{evento.campionato ? " · Campionato" : " · Amichevole"}
|
||||||
</p>
|
</p>
|
||||||
@@ -126,7 +126,7 @@ function PartitaDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 space-y-2 text-sm text-muted-foreground">
|
<div className="mt-4 space-y-2 text-[15px] text-muted-foreground">
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4" /> {evento.ora}
|
<Clock className="h-4 w-4" /> {evento.ora}
|
||||||
</span>
|
</span>
|
||||||
@@ -136,12 +136,12 @@ function PartitaDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{evento.note ? (
|
{evento.note ? (
|
||||||
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-sm">
|
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-[15px]">
|
||||||
{evento.note}
|
{evento.note}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1.5 text-xs font-semibold">
|
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1.5 text-[13px] font-semibold">
|
||||||
<Users className="h-4 w-4" />
|
<Users className="h-4 w-4" />
|
||||||
Conferme: {presentiVeri}/{convocati.length}
|
Conferme: {presentiVeri}/{convocati.length}
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +163,7 @@ function PartitaDetail() {
|
|||||||
<Section titolo="Risultato">
|
<Section titolo="Risultato">
|
||||||
<div className="rounded-3xl bg-card p-5 shadow-card">
|
<div className="rounded-3xl bg-card p-5 shadow-card">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-bold">CRAP Volley</span>
|
<span className="text-[15px] font-bold">CRAP Volley</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1 text-lg font-display font-bold",
|
"rounded-full px-3 py-1 text-lg font-display font-bold",
|
||||||
@@ -174,7 +174,7 @@ function PartitaDetail() {
|
|||||||
>
|
>
|
||||||
{match.setNostri} - {match.setLoro}
|
{match.setNostri} - {match.setLoro}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-right text-sm font-bold">{avversario}</span>
|
<span className="text-right text-[15px] font-bold">{avversario}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
{match.parziali.map(([noi, loro], i) => (
|
{match.parziali.map(([noi, loro], i) => (
|
||||||
@@ -183,7 +183,9 @@ function PartitaDetail() {
|
|||||||
className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2"
|
className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2"
|
||||||
>
|
>
|
||||||
<span className="font-display text-lg">{noi}</span>
|
<span className="font-display text-lg">{noi}</span>
|
||||||
<span className="text-xs font-semibold text-muted-foreground">Set {i + 1}</span>
|
<span className="text-[13px] font-semibold text-muted-foreground">
|
||||||
|
Set {i + 1}
|
||||||
|
</span>
|
||||||
<span className="font-display text-lg">{loro}</span>
|
<span className="font-display text-lg">{loro}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -213,7 +215,7 @@ function PartitaDetail() {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Section titolo="In programma">
|
<Section titolo="In programma">
|
||||||
<p className="rounded-3xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
<p className="rounded-3xl bg-card p-5 text-center text-[15px] text-muted-foreground shadow-card">
|
||||||
La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il
|
La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il
|
||||||
risultato.
|
risultato.
|
||||||
</p>
|
</p>
|
||||||
@@ -248,13 +250,13 @@ function PartitaDetail() {
|
|||||||
].map((t) => (
|
].map((t) => (
|
||||||
<div key={t.l} className="rounded-2xl bg-secondary p-2.5">
|
<div key={t.l} className="rounded-2xl bg-secondary p-2.5">
|
||||||
<p className="font-display text-xl leading-none">{t.v}</p>
|
<p className="font-display text-xl leading-none">{t.v}</p>
|
||||||
<p className="mt-1 text-xs font-semibold uppercase text-muted-foreground">
|
<p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground">
|
||||||
{t.l}
|
{t.l}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mt-4 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Dettaglio giocatori (uso interno allenatori)
|
Dettaglio giocatori (uso interno allenatori)
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
@@ -264,7 +266,7 @@ function PartitaDetail() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={gid}
|
key={gid}
|
||||||
className="flex items-center gap-2 rounded-xl bg-secondary/60 px-3 py-2 text-xs"
|
className="flex items-center gap-2 rounded-xl bg-secondary/60 px-3 py-2 text-[13px]"
|
||||||
>
|
>
|
||||||
<span className="min-w-0 flex-1 truncate font-semibold">
|
<span className="min-w-0 flex-1 truncate font-semibold">
|
||||||
#{g.numero} {g.nome}
|
#{g.numero} {g.nome}
|
||||||
@@ -285,7 +287,7 @@ function PartitaDetail() {
|
|||||||
csvScoutMatch(scout, rosa),
|
csvScoutMatch(scout, rosa),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" /> Esporta CSV
|
<Download className="h-4 w-4" /> Esporta CSV
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+10
-10
@@ -220,7 +220,7 @@ function CardAvatar({ id, fallback }: { id: string; fallback: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
className="premi min-h-11 flex-1 rounded-xl bg-secondary px-3 text-xs font-bold uppercase tracking-wide"
|
className="premi min-h-11 flex-1 rounded-xl bg-secondary px-3 text-[13px] font-bold uppercase tracking-wide"
|
||||||
>
|
>
|
||||||
Cambia immagine profilo
|
Cambia immagine profilo
|
||||||
</button>
|
</button>
|
||||||
@@ -312,11 +312,11 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={cambiaNotifiche}
|
onClick={cambiaNotifiche}
|
||||||
disabled={!supportate || inCorso}
|
disabled={!supportate || inCorso}
|
||||||
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm disabled:opacity-60"
|
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-[15px] disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="block truncate">Notifiche</span>
|
<span className="block truncate">Notifiche</span>
|
||||||
<span className="block text-xs text-muted-foreground">
|
<span className="block text-[13px] text-muted-foreground">
|
||||||
{supportate
|
{supportate
|
||||||
? notifiche
|
? notifiche
|
||||||
? "Attive su questo dispositivo"
|
? "Attive su questo dispositivo"
|
||||||
@@ -339,11 +339,11 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={cambiaEmail}
|
onClick={cambiaEmail}
|
||||||
disabled={!email.pronta || email.imposta.isPending}
|
disabled={!email.pronta || email.imposta.isPending}
|
||||||
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm disabled:opacity-60"
|
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-left text-[15px] disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
<span className="block truncate">Email</span>
|
<span className="block truncate">Email</span>
|
||||||
<span className="block text-xs text-muted-foreground">
|
<span className="block text-[13px] text-muted-foreground">
|
||||||
{email.attive
|
{email.attive
|
||||||
? "Le notifiche arrivano anche via email"
|
? "Le notifiche arrivano anche via email"
|
||||||
: "Nessuna email, solo notifiche in app"}
|
: "Nessuna email, solo notifiche in app"}
|
||||||
@@ -363,7 +363,7 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
|||||||
{admin ? (
|
{admin ? (
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
|
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-[15px] transition-colors hover:bg-accent/5"
|
||||||
>
|
>
|
||||||
<span className="min-w-0 truncate">Dashboard amministratore</span>
|
<span className="min-w-0 truncate">Dashboard amministratore</span>
|
||||||
<ShieldCheck className="h-4 w-4 text-muted-foreground" />
|
<ShieldCheck className="h-4 w-4 text-muted-foreground" />
|
||||||
@@ -373,7 +373,7 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
|||||||
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=bug_report.yml"
|
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=bug_report.yml"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
|
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-[15px] transition-colors hover:bg-accent/5"
|
||||||
>
|
>
|
||||||
<span className="min-w-0 truncate">Segnala un bug</span>
|
<span className="min-w-0 truncate">Segnala un bug</span>
|
||||||
<Bug className="h-4 w-4 text-muted-foreground" />
|
<Bug className="h-4 w-4 text-muted-foreground" />
|
||||||
@@ -382,17 +382,17 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) {
|
|||||||
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=feature_request.yml"
|
href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=feature_request.yml"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-sm transition-colors hover:bg-accent/5"
|
className="flex min-h-11 w-full items-center justify-between gap-3 px-4 py-3 text-[15px] transition-colors hover:bg-accent/5"
|
||||||
>
|
>
|
||||||
<span className="min-w-0 truncate">Suggerisci una nuova funzionalità</span>
|
<span className="min-w-0 truncate">Suggerisci una nuova funzionalità</span>
|
||||||
<Lightbulb className="h-4 w-4 text-muted-foreground" />
|
<Lightbulb className="h-4 w-4 text-muted-foreground" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center text-xs text-muted-foreground">CrAPP v{APP_VERSION}</p>
|
<p className="text-center text-[13px] text-muted-foreground">CrAPP v{APP_VERSION}</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="premi flex min-h-11 w-full items-center justify-center gap-2 rounded-3xl bg-destructive/15 px-4 py-3 text-sm font-bold text-destructive shadow-card"
|
className="premi flex min-h-11 w-full items-center justify-center gap-2 rounded-3xl bg-destructive/15 px-4 py-3 text-[15px] font-bold text-destructive shadow-card"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
Esci
|
Esci
|
||||||
|
|||||||
+23
-23
@@ -72,7 +72,7 @@ function Blocco({
|
|||||||
{icona}
|
{icona}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mt-4 font-display text-2xl uppercase leading-none">{titolo}</h1>
|
<h1 className="mt-4 font-display text-2xl uppercase leading-none">{titolo}</h1>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">{testo}</p>
|
<p className="mt-2 text-[15px] text-muted-foreground">{testo}</p>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +138,7 @@ function Scout() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => sessione.refetch()}
|
onClick={() => sessione.refetch()}
|
||||||
className="mt-5 w-full rounded-2xl bg-secondary py-3 text-sm font-bold uppercase"
|
className="mt-5 w-full rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase"
|
||||||
>
|
>
|
||||||
Aggiorna
|
Aggiorna
|
||||||
</button>
|
</button>
|
||||||
@@ -156,7 +156,7 @@ function Scout() {
|
|||||||
if (ok) setControllo(true);
|
if (ok) setControllo(true);
|
||||||
else toast.error("Un altro compagno ha appena preso lo scout");
|
else toast.error("Un altro compagno ha appena preso lo scout");
|
||||||
}}
|
}}
|
||||||
className="mt-5 w-full rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
className="mt-5 w-full rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{ripresa ? "Riprendi lo scout" : "Prendi il controllo"}
|
{ripresa ? "Riprendi lo scout" : "Prendi il controllo"}
|
||||||
</button>
|
</button>
|
||||||
@@ -313,7 +313,7 @@ function ScoutBoard({
|
|||||||
<>
|
<>
|
||||||
<header className="sticky top-0 z-30 bg-hero px-5 pb-4 pt-6 text-primary-foreground">
|
<header className="sticky top-0 z-30 bg-hero px-5 pb-4 pt-6 text-primary-foreground">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary-foreground/10 px-2.5 py-1 text-xs font-bold uppercase tracking-wide">
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary-foreground/10 px-2.5 py-1 text-[13px] font-bold uppercase tracking-wide">
|
||||||
<Radio className="h-3 w-3 text-accent" /> Scout live · Set {setCorrente}
|
<Radio className="h-3 w-3 text-accent" /> Scout live · Set {setCorrente}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -321,14 +321,14 @@ function ScoutBoard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={annulla}
|
onClick={annulla}
|
||||||
disabled={azioniSet.length === 0}
|
disabled={azioniSet.length === 0}
|
||||||
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-xs font-bold disabled:opacity-40"
|
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-[13px] font-bold disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<Undo2 className="h-3.5 w-3.5" /> Annulla
|
<Undo2 className="h-3.5 w-3.5" /> Annulla
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onFine}
|
onClick={onFine}
|
||||||
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-xs font-bold"
|
className="inline-flex items-center gap-1 rounded-full bg-primary-foreground/10 px-3 py-1.5 text-[13px] font-bold"
|
||||||
>
|
>
|
||||||
<LogOut className="h-3.5 w-3.5" /> Rilascia
|
<LogOut className="h-3.5 w-3.5" /> Rilascia
|
||||||
</button>
|
</button>
|
||||||
@@ -337,14 +337,14 @@ function ScoutBoard({
|
|||||||
|
|
||||||
<div className="mt-3 grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-center">
|
<div className="mt-3 grid grid-cols-[1fr_auto_1fr] items-center gap-2 text-center">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-xs uppercase text-primary-foreground/60">CRAP Volley</p>
|
<p className="truncate text-[13px] uppercase text-primary-foreground/60">CRAP Volley</p>
|
||||||
<p className="font-display text-5xl leading-none text-accent">{puntiNoi}</p>
|
<p className="font-display text-5xl leading-none text-accent">{puntiNoi}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-display text-2xl leading-none text-primary-foreground/50">
|
<p className="font-display text-2xl leading-none text-primary-foreground/50">
|
||||||
{setNostri}-{setLoro}
|
{setNostri}-{setLoro}
|
||||||
</p>
|
</p>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-xs uppercase text-primary-foreground/60">
|
<p className="truncate text-[13px] uppercase text-primary-foreground/60">
|
||||||
{avversario || "Avversario"}
|
{avversario || "Avversario"}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-display text-5xl leading-none">{puntiLoro}</p>
|
<p className="font-display text-5xl leading-none">{puntiLoro}</p>
|
||||||
@@ -356,7 +356,7 @@ function ScoutBoard({
|
|||||||
{setChiusi.map((p, i) => (
|
{setChiusi.map((p, i) => (
|
||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className="rounded-lg bg-primary-foreground/10 px-2 py-0.5 text-xs font-semibold tabular-nums"
|
className="rounded-lg bg-primary-foreground/10 px-2 py-0.5 text-[13px] font-semibold tabular-nums"
|
||||||
>
|
>
|
||||||
{p[0]}-{p[1]}
|
{p[0]}-{p[1]}
|
||||||
</span>
|
</span>
|
||||||
@@ -366,7 +366,7 @@ function ScoutBoard({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section className="px-5 pt-4">
|
<section className="px-5 pt-4">
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
1. Tocca il giocatore
|
1. Tocca il giocatore
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
@@ -384,10 +384,10 @@ function ScoutBoard({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="font-display text-2xl leading-none">{g.numero}</p>
|
<p className="font-display text-2xl leading-none">{g.numero}</p>
|
||||||
<p className="mt-1 text-xs font-bold leading-tight">{g.nome}</p>
|
<p className="mt-1 text-[13px] font-bold leading-tight">{g.nome}</p>
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs tabular-nums",
|
"text-[13px] tabular-nums",
|
||||||
attivo ? "text-accent-foreground/70" : "text-muted-foreground",
|
attivo ? "text-accent-foreground/70" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -400,7 +400,7 @@ function ScoutBoard({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="px-5 pt-4">
|
<section className="px-5 pt-4">
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mb-2 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
2. Tocca l'azione
|
2. Tocca l'azione
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -426,7 +426,7 @@ function ScoutBoard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => registra(tipo)}
|
onClick={() => registra(tipo)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl py-3 text-sm font-bold uppercase shadow-card transition-transform active:scale-95",
|
"rounded-2xl py-3 text-[15px] font-bold uppercase shadow-card transition-transform active:scale-95",
|
||||||
azioniMeta[tipo].className,
|
azioniMeta[tipo].className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -438,11 +438,11 @@ function ScoutBoard({
|
|||||||
|
|
||||||
<section className="px-5 pt-4">
|
<section className="px-5 pt-4">
|
||||||
<div className="rounded-3xl bg-card p-3 shadow-card">
|
<div className="rounded-3xl bg-card p-3 shadow-card">
|
||||||
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Ultime azioni
|
Ultime azioni
|
||||||
</p>
|
</p>
|
||||||
{ultime.length === 0 ? (
|
{ultime.length === 0 ? (
|
||||||
<p className="mt-2 text-xs text-muted-foreground">Nessuna azione registrata.</p>
|
<p className="mt-2 text-[13px] text-muted-foreground">Nessuna azione registrata.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="mt-2 space-y-1.5">
|
<ul className="mt-2 space-y-1.5">
|
||||||
{ultime.map((a, i) => {
|
{ultime.map((a, i) => {
|
||||||
@@ -451,14 +451,14 @@ function ScoutBoard({
|
|||||||
<li
|
<li
|
||||||
key={a.id}
|
key={a.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-between gap-2 px-1 text-xs",
|
"flex items-center justify-between gap-2 px-1 text-[13px]",
|
||||||
i === 0 && "anim-riga",
|
i === 0 && "anim-riga",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span>
|
<span className="truncate">{g ? `#${g.numero} ${g.nome}` : "Avversario"}</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 rounded-full px-2 py-0.5 text-xs font-bold uppercase",
|
"shrink-0 rounded-full px-2 py-0.5 text-[13px] font-bold uppercase",
|
||||||
azioniMeta[a.tipo].className,
|
azioniMeta[a.tipo].className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -474,14 +474,14 @@ function ScoutBoard({
|
|||||||
|
|
||||||
<section className="px-5 pt-4">
|
<section className="px-5 pt-4">
|
||||||
<Card>
|
<Card>
|
||||||
<label className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<label className="text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Avversario
|
Avversario
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={avversario}
|
value={avversario}
|
||||||
maxLength={40}
|
maxLength={40}
|
||||||
onChange={(e) => setAvversario(e.target.value)}
|
onChange={(e) => setAvversario(e.target.value)}
|
||||||
className="mt-1 w-full rounded-xl border border-border bg-background px-3 py-2 text-sm"
|
className="mt-1 w-full rounded-xl border border-border bg-background px-3 py-2 text-[15px]"
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 flex rounded-full bg-secondary p-1">
|
<div className="mt-3 flex rounded-full bg-secondary p-1">
|
||||||
{[true, false].map((v) => (
|
{[true, false].map((v) => (
|
||||||
@@ -490,7 +490,7 @@ function ScoutBoard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCasa(v)}
|
onClick={() => setCasa(v)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 rounded-full py-1.5 text-xs font-bold uppercase",
|
"flex-1 rounded-full py-1.5 text-[13px] font-bold uppercase",
|
||||||
casa === v ? "bg-card text-foreground shadow-card" : "text-muted-foreground",
|
casa === v ? "bg-card text-foreground shadow-card" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -505,7 +505,7 @@ function ScoutBoard({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={chiudiSet}
|
onClick={chiudiSet}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-secondary py-3 text-sm font-bold uppercase"
|
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-secondary py-3 text-[15px] font-bold uppercase"
|
||||||
>
|
>
|
||||||
<CheckCircle2 className="h-4 w-4" /> Chiudi set
|
<CheckCircle2 className="h-4 w-4" /> Chiudi set
|
||||||
</button>
|
</button>
|
||||||
@@ -513,7 +513,7 @@ function ScoutBoard({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={finePartita}
|
onClick={finePartita}
|
||||||
disabled={salvaMatch.isPending}
|
disabled={salvaMatch.isPending}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
className="inline-flex items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-[15px] font-bold uppercase text-accent-foreground shadow-pop disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Save className="h-4 w-4" /> Fine partita
|
<Save className="h-4 w-4" /> Fine partita
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+73
-56
@@ -1,11 +1,12 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { Cake, ChevronDown, Crown, Trophy } from "lucide-react";
|
import { Cake, ChevronDown, ChevronRight, Crown, Trophy } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PageHeader, StatTile } from "@/components/crapp/ui-bits";
|
import { PageHeader, StatTile } from "@/components/crapp/ui-bits";
|
||||||
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
import { BarraSottosezioni } from "@/components/crapp/BarraSottosezioni";
|
||||||
import { Avatar } from "@/components/crapp/Avatar";
|
import { Avatar } from "@/components/crapp/Avatar";
|
||||||
import { formatData, inizialiDa } from "@/lib/crapp-data";
|
import { formatData, inizialiDa } from "@/lib/crapp-data";
|
||||||
|
import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer";
|
||||||
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi";
|
||||||
import {
|
import {
|
||||||
useRosa,
|
useRosa,
|
||||||
@@ -50,7 +51,7 @@ import {
|
|||||||
|
|
||||||
function RuoloBadge({ ruolo }: { ruolo: string }) {
|
function RuoloBadge({ ruolo }: { ruolo: string }) {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center rounded-full bg-accent px-2 py-0.5 text-xs font-bold uppercase tracking-wide text-accent-foreground">
|
<span className="inline-flex items-center rounded-full bg-accent px-2 py-0.5 text-[13px] font-bold uppercase tracking-wide text-accent-foreground">
|
||||||
{ruolo}
|
{ruolo}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -146,7 +147,7 @@ function Squadra() {
|
|||||||
className="h-11 w-11 text-lg"
|
className="h-11 w-11 text-lg"
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-bold leading-tight">
|
<span className="block truncate text-[15px] font-bold leading-tight">
|
||||||
{nomeCompleto(a)}
|
{nomeCompleto(a)}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 flex items-center gap-2">
|
<span className="mt-1 flex items-center gap-2">
|
||||||
@@ -163,7 +164,7 @@ function Squadra() {
|
|||||||
|
|
||||||
{isOpen && a.nascita ? (
|
{isOpen && a.nascita ? (
|
||||||
<div className="border-t border-border px-4 pb-4 pt-3">
|
<div className="border-t border-border px-4 pb-4 pt-3">
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
<p className="inline-flex items-center gap-1.5 text-[13px] text-muted-foreground">
|
||||||
<Cake className="h-3.5 w-3.5" /> {formatData(a.nascita)}
|
<Cake className="h-3.5 w-3.5" /> {formatData(a.nascita)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,10 +192,12 @@ function Squadra() {
|
|||||||
className="h-11 w-11 text-lg"
|
className="h-11 w-11 text-lg"
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-bold leading-tight">{g.nome}</span>
|
<span className="block truncate text-[15px] font-bold leading-tight">
|
||||||
|
{g.nome}
|
||||||
|
</span>
|
||||||
<span className="mt-1 flex items-center gap-2">
|
<span className="mt-1 flex items-center gap-2">
|
||||||
<RuoloBadge ruolo={g.ruolo} />
|
<RuoloBadge ruolo={g.ruolo} />
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-[13px] text-muted-foreground">
|
||||||
{g.numero ? `#${g.numero}` : "n° da definire"}
|
{g.numero ? `#${g.numero}` : "n° da definire"}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -229,7 +232,7 @@ function Squadra() {
|
|||||||
<div className="border-t border-border px-4 pb-4 pt-3">
|
<div className="border-t border-border px-4 pb-4 pt-3">
|
||||||
{g.nascita ? (
|
{g.nascita ? (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<p className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
|
<p className="inline-flex items-center gap-1.5 text-[13px] text-muted-foreground">
|
||||||
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)}{" "}
|
<Cake className="h-3.5 w-3.5" /> {formatData(g.nascita)}{" "}
|
||||||
{g.nascita.slice(0, 4)}
|
{g.nascita.slice(0, 4)}
|
||||||
</p>
|
</p>
|
||||||
@@ -243,13 +246,11 @@ function Squadra() {
|
|||||||
{ l: "Presenze di fila", v: g.streak },
|
{ l: "Presenze di fila", v: g.streak },
|
||||||
{ l: "Media voto", v: g.mediaVoto || "—" },
|
{ l: "Media voto", v: g.mediaVoto || "—" },
|
||||||
{ l: "MVP", v: g.mvp },
|
{ l: "MVP", v: g.mvp },
|
||||||
...(sonoAllenatore
|
...(sonoAllenatore ? [] : [{ l: "Cacche 💩", v: g.cacchePartita || "—" }]),
|
||||||
? []
|
|
||||||
: [{ l: "Cacche/partita 💩", v: g.cacchePartita || "—" }]),
|
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div key={s.l} className="rounded-2xl bg-secondary p-2.5 text-center">
|
<div key={s.l} className="rounded-2xl bg-secondary p-2.5 text-center">
|
||||||
<p className="font-display text-xl leading-none">{s.v}</p>
|
<p className="font-display text-xl leading-none">{s.v}</p>
|
||||||
<p className="mt-1 text-xs font-semibold uppercase text-muted-foreground">
|
<p className="mt-1 text-[13px] font-semibold uppercase text-muted-foreground">
|
||||||
{s.l}
|
{s.l}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,11 +259,11 @@ function Squadra() {
|
|||||||
|
|
||||||
{sonoAllenatore ? null : (
|
{sonoAllenatore ? null : (
|
||||||
<>
|
<>
|
||||||
<p className="mt-4 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
<p className="mt-4 text-[13px] font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
Badge sbloccati · {collezioneBadge(g).ottenuti}/{collezioneBadge(g).totali}
|
Badge sbloccati · {collezioneBadge(g).ottenuti}/{collezioneBadge(g).totali}
|
||||||
</p>
|
</p>
|
||||||
{sbloccati.length === 0 ? (
|
{sbloccati.length === 0 ? (
|
||||||
<p className="mt-2 rounded-2xl bg-secondary/50 p-3 text-xs text-muted-foreground">
|
<p className="mt-2 rounded-2xl bg-secondary/50 p-3 text-[13px] text-muted-foreground">
|
||||||
Nessun badge sbloccato per ora.
|
Nessun badge sbloccato per ora.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -274,17 +275,20 @@ function Squadra() {
|
|||||||
<BadgeDrawer key={b.def.id} def={b.def} stato={b}>
|
<BadgeDrawer key={b.def.id} def={b.def} stato={b}>
|
||||||
<div className={cn("rounded-2xl p-2.5 ring-1", meta.bg, meta.ring)}>
|
<div className={cn("rounded-2xl p-2.5 ring-1", meta.bg, meta.ring)}>
|
||||||
<Icon className={cn("h-4 w-4", meta.text)} />
|
<Icon className={cn("h-4 w-4", meta.text)} />
|
||||||
<p className="mt-1 text-xs font-bold leading-tight">
|
<p className="mt-1 text-[13px] font-bold leading-tight">
|
||||||
{b.def.nome}
|
{b.def.nome}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{b.valore} {b.def.unita}
|
{b.valore} {b.def.unita}
|
||||||
{b.prossimaSoglia
|
{b.prossimaSoglia
|
||||||
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
|
? ` · ${b.prossimaSoglia} per ${gradoMeta[b.prossimo!].label.toLowerCase()}`
|
||||||
: ""}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={cn("mt-1.5 text-xs font-bold uppercase", meta.text)}
|
className={cn(
|
||||||
|
"mt-1.5 text-[13px] font-bold uppercase",
|
||||||
|
meta.text,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{meta.label}
|
{meta.label}
|
||||||
</p>
|
</p>
|
||||||
@@ -326,8 +330,8 @@ function Squadra() {
|
|||||||
className="flex w-full min-w-0 items-center gap-2 rounded-2xl bg-card px-3 py-2.5 text-left shadow-card"
|
className="flex w-full min-w-0 items-center gap-2 rounded-2xl bg-card px-3 py-2.5 text-left shadow-card"
|
||||||
>
|
>
|
||||||
<Trophy className="h-5 w-5 shrink-0 text-warning" aria-hidden />
|
<Trophy className="h-5 w-5 shrink-0 text-warning" aria-hidden />
|
||||||
<span className="shrink-0 text-[14px] text-foreground/80">Classifica per</span>
|
<span className="shrink-0 text-[15px] text-foreground/80">Classifica per</span>
|
||||||
<span className="min-w-0 flex-1 truncate text-right text-[14px] font-bold">
|
<span className="min-w-0 flex-1 truncate text-right text-[15px] font-bold">
|
||||||
{criteri.find((c) => c.id === criterio)?.label}
|
{criteri.find((c) => c.id === criterio)?.label}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
||||||
@@ -345,7 +349,7 @@ function Squadra() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCriterio(c.id)}
|
onClick={() => setCriterio(c.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-11 w-full items-center rounded-xl px-3 text-left text-sm font-bold",
|
"flex min-h-11 w-full items-center rounded-xl px-3 text-left text-[15px] font-bold",
|
||||||
criterio === c.id
|
criterio === c.id
|
||||||
? "bg-accent text-accent-foreground"
|
? "bg-accent text-accent-foreground"
|
||||||
: "bg-secondary text-foreground",
|
: "bg-secondary text-foreground",
|
||||||
@@ -368,13 +372,13 @@ function Squadra() {
|
|||||||
{rank[i]}
|
{rank[i]}
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-bold">
|
<p className="truncate text-[15px] font-bold">
|
||||||
{g.nome}
|
{g.nome}
|
||||||
{rank[i] === 1 ? (
|
{rank[i] === 1 ? (
|
||||||
<Crown className="ml-1 inline h-3.5 w-3.5 text-warning" />
|
<Crown className="ml-1 inline h-3.5 w-3.5 text-warning" />
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
<p className="truncate text-[13px] text-muted-foreground">
|
||||||
#{g.numero} · {g.ruolo} · {dettaglioClassifica(g, criterio)}
|
#{g.numero} · {g.ruolo} · {dettaglioClassifica(g, criterio)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -414,14 +418,14 @@ function Squadra() {
|
|||||||
<div className="mb-3 rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
|
<div className="mb-3 rounded-3xl bg-hero p-4 text-primary-foreground shadow-card">
|
||||||
<div className="flex items-end justify-between gap-3">
|
<div className="flex items-end justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-primary-foreground/60">
|
<p className="text-[13px] font-semibold uppercase tracking-wide text-primary-foreground/60">
|
||||||
Progresso collettivo
|
Progresso collettivo
|
||||||
</p>
|
</p>
|
||||||
<p className="font-display text-4xl leading-none">
|
<p className="font-display text-4xl leading-none">
|
||||||
<Numero valore={mediaObiettivi} suffisso="%" />
|
<Numero valore={mediaObiettivi} suffisso="%" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-primary-foreground/70">
|
<p className="text-[13px] text-primary-foreground/70">
|
||||||
{completati}/{obiettivi.length} completati
|
{completati}/{obiettivi.length} completati
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,38 +436,47 @@ function Squadra() {
|
|||||||
const pct = progressoObiettivo(o);
|
const pct = progressoObiettivo(o);
|
||||||
const fatto = pct >= 100;
|
const fatto = pct >= 100;
|
||||||
return (
|
return (
|
||||||
<Reveal
|
<Reveal key={o.id} indice={i}>
|
||||||
key={o.id}
|
<ObiettivoDrawer obiettivo={o}>
|
||||||
indice={i}
|
<button
|
||||||
className={cn(
|
type="button"
|
||||||
"premi rounded-3xl bg-card p-4 shadow-card ring-1",
|
aria-label={`Dettaglio obiettivo: ${o.titolo}`}
|
||||||
fatto ? "ring-success/40" : pct >= 90 ? "ring-accent/40" : "ring-transparent",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="text-lg leading-none">{o.emoji}</span>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-sm font-bold leading-tight">{o.titolo}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">{o.descrizione}</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-2 py-0.5 text-xs font-bold uppercase tracking-wide",
|
"premi block w-full rounded-3xl bg-card p-4 text-left shadow-card ring-1",
|
||||||
fatto
|
fatto ? "ring-success/40" : pct >= 90 ? "ring-accent/40" : "ring-transparent",
|
||||||
? "bg-success text-success-foreground"
|
|
||||||
: "bg-secondary text-muted-foreground",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{fatto ? "Completato" : `${pct}%`}
|
<div className="flex items-start gap-2">
|
||||||
</span>
|
<span className="text-lg leading-none">{o.emoji}</span>
|
||||||
</div>
|
<div className="min-w-0 flex-1">
|
||||||
<Barra percentuale={pct} trackClassName="mt-3" />
|
<p className="text-[15px] font-bold leading-tight">{o.titolo}</p>
|
||||||
<p className="mt-2 text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">{o.descrizione}</p>
|
||||||
{o.valore}/{o.target} {o.unita} · {pct}%
|
</div>
|
||||||
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
|
<span
|
||||||
</p>
|
className={cn(
|
||||||
<p className="mt-1 text-xs font-semibold text-accent">{microcopyObiettivo(o)}</p>
|
"rounded-full px-2 py-0.5 text-[13px] font-bold uppercase tracking-wide",
|
||||||
<p className="mt-0.5 text-xs text-muted-foreground">{o.impatto}</p>
|
fatto
|
||||||
|
? "bg-success text-success-foreground"
|
||||||
|
: "bg-secondary text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{fatto ? "Completato" : `${pct}%`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Barra percentuale={pct} trackClassName="mt-3" />
|
||||||
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
|
{o.valore}/{o.target} {o.unita} · {pct}%
|
||||||
|
{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[13px] font-semibold text-accent">
|
||||||
|
{microcopyObiettivo(o)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 text-[13px] text-muted-foreground">{o.impatto}</p>
|
||||||
|
<p className="mt-2 inline-flex items-center text-[13px] font-semibold text-accent">
|
||||||
|
Come funziona <ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</ObiettivoDrawer>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -483,8 +496,10 @@ function Squadra() {
|
|||||||
<div className="rounded-3xl bg-card p-3 shadow-card">
|
<div className="rounded-3xl bg-card p-3 shadow-card">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon className="h-5 w-5 text-accent" />
|
<Icon className="h-5 w-5 text-accent" />
|
||||||
<p className="text-sm font-bold leading-tight">{b.nome}</p>
|
<p className="text-[15px] font-bold leading-tight">{b.nome}</p>
|
||||||
<p className="ml-auto text-xs text-muted-foreground">{descrizioneSoglie(b)}</p>
|
<p className="ml-auto text-[13px] text-muted-foreground">
|
||||||
|
{descrizioneSoglie(b)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 grid grid-cols-3 gap-1.5">
|
<div className="mt-2 grid grid-cols-3 gap-1.5">
|
||||||
{gradiOrdine.map((grado) => {
|
{gradiOrdine.map((grado) => {
|
||||||
@@ -500,9 +515,11 @@ function Squadra() {
|
|||||||
key={grado}
|
key={grado}
|
||||||
className={cn("rounded-2xl p-2 text-center ring-1", meta.bg, meta.ring)}
|
className={cn("rounded-2xl p-2 text-center ring-1", meta.bg, meta.ring)}
|
||||||
>
|
>
|
||||||
<p className={cn("text-xs font-bold uppercase", meta.text)}>{meta.label}</p>
|
<p className={cn("text-[13px] font-bold uppercase", meta.text)}>
|
||||||
|
{meta.label}
|
||||||
|
</p>
|
||||||
<p className="font-display text-lg leading-none">{b.soglie[grado]}</p>
|
<p className="font-display text-lg leading-none">{b.soglie[grado]}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[13px] text-muted-foreground">
|
||||||
{quanti}/{rosa.length}
|
{quanti}/{rosa.length}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
|
|||||||
|
|
||||||
| Cartella | Cosa verifica | Serve rete? |
|
| Cartella | Cosa verifica | Serve rete? |
|
||||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||||
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No |
|
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi (calcolo e card di dettaglio), avvisi in Home, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No |
|
||||||
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26-M28 e M30, DD-040, DD-042, DD-043 e DD-045: palloni un solo avviso a 3 ore, sollecito uno solo a 24 ore, nessun promemoria evento) la colonna `foto_path` eliminata (`foto-tessera-rimossa`, M29, DD-044) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì |
|
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26-M28 e M30, DD-040, DD-042, DD-043 e DD-045: palloni un solo avviso a 3 ore, sollecito uno solo a 24 ore, nessun promemoria evento) la colonna `foto_path` eliminata (`foto-tessera-rimossa`, M29, DD-044) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì |
|
||||||
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
||||||
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Obiettivi di squadra end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`.
|
* Obiettivi di squadra end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`.
|
||||||
* Copre gli obiettivi con scadenza/mese dinamici ("presenze del mese", "evento di squadra al
|
* Copre gli obiettivi con scadenza/mese dinamici ("presenze del mese", "evento di squadra al
|
||||||
* mese", "tutti rispondono alle convocazioni") e quelli la cui logica dipende da dati scritti
|
* mese", "tutti rispondono alle convocazioni") e quelli la cui logica dipende da dati scritti
|
||||||
* su altre tabelle ("250 presenze complessive" via `contaPresenzeGiocatore()`, "media pagelle
|
* su altre tabelle ("presenze collettive" via `leggiEventi()`/`leggiPresenze()`, "media pagelle
|
||||||
* da 7.5" e "200 pagelle compilate" via `pagelle_voti`).
|
* da 7.5" e "200 pagelle compilate" via `pagelle_voti`).
|
||||||
*
|
*
|
||||||
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
|
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { giocatori } from "@/lib/crapp-data";
|
import { giocatori } from "@/lib/crapp-data";
|
||||||
import { obiettiviSquadra } from "@/lib/obiettivi";
|
import { obiettiviSquadra } from "@/lib/obiettivi";
|
||||||
import { contaPresenzeGiocatore, serieConsecutiva, type MappaPresenze } from "@/lib/presenze";
|
import { serieConsecutiva, type MappaPresenze } from "@/lib/presenze";
|
||||||
import { statoLocale } from "../helpers/locale";
|
import { statoLocale } from "../helpers/locale";
|
||||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
@@ -282,33 +282,51 @@ if (!locale) {
|
|||||||
"o6 legge dal database l'evento sociale del mese, azzerandosi come o1",
|
"o6 legge dal database l'evento sociale del mese, azzerandosi come o1",
|
||||||
async () => {
|
async () => {
|
||||||
const pizzataId = `${PREFISSO}-pizzata`;
|
const pizzataId = `${PREFISSO}-pizzata`;
|
||||||
const inserita = await rest("eventi_app", {
|
const pizzataFuturaId = `${PREFISSO}-pizzata-futura`;
|
||||||
method: "POST",
|
// OGGI è il 15 marzo 12:00 a Roma: la pizzata del 10 è passata, quella del 20 no.
|
||||||
body: JSON.stringify({
|
for (const [id, giorno] of [
|
||||||
id: pizzataId,
|
[pizzataId, "10"],
|
||||||
tipo: "evento",
|
[pizzataFuturaId, "20"],
|
||||||
titolo: "Test obiettivi o6",
|
] as const) {
|
||||||
data: `${MESE_TEST}-20`,
|
const inserita = await rest("eventi_app", {
|
||||||
}),
|
method: "POST",
|
||||||
});
|
body: JSON.stringify({
|
||||||
if (!inserita.ok) throw new Error(`inserimento evento fallito: ${await inserita.text()}`);
|
id,
|
||||||
|
tipo: "evento",
|
||||||
|
titolo: "Test obiettivi o6",
|
||||||
|
data: `${MESE_TEST}-${giorno}`,
|
||||||
|
ora: "20:30",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!inserita.ok) throw new Error(`inserimento evento fallito: ${await inserita.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
const eventiReali = await leggiEventi();
|
const tutti = await leggiEventi();
|
||||||
const o6InMese = obiettiviSquadra(
|
const o6Di = (ids: string[], oggi: Date) =>
|
||||||
giocatori,
|
obiettiviSquadra(
|
||||||
{ eventi: eventiReali, presenze: {}, pagelle: [] },
|
giocatori,
|
||||||
OGGI,
|
{ eventi: tutti.filter((e) => ids.includes(e.id)), presenze: {}, pagelle: [] },
|
||||||
).find((o) => o.id === "o6")!;
|
oggi,
|
||||||
assert.equal(o6InMese.valore, 1, "l'evento sociale di marzo conta letto dal database");
|
).find((o) => o.id === "o6")!;
|
||||||
|
|
||||||
const OGGI_MESE_DOPO = new Date("2099-04-15T10:00:00Z");
|
|
||||||
const o6MeseDopo = obiettiviSquadra(
|
|
||||||
giocatori,
|
|
||||||
{ eventi: eventiReali, presenze: {}, pagelle: [] },
|
|
||||||
OGGI_MESE_DOPO,
|
|
||||||
).find((o) => o.id === "o6")!;
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
o6MeseDopo.valore,
|
o6Di([pizzataId], OGGI).valore,
|
||||||
|
1,
|
||||||
|
"l'evento sociale già passato conta, letto dal database",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
o6Di([pizzataFuturaId], OGGI).valore,
|
||||||
|
0,
|
||||||
|
"l'evento sociale futuro non conta finché non arriva la sua ora",
|
||||||
|
);
|
||||||
|
// Il 20 marzo 2099 alle 20:30 Roma (CET, UTC+1) = 19:30Z: da lì conta.
|
||||||
|
assert.equal(
|
||||||
|
o6Di([pizzataFuturaId], new Date("2099-03-20T19:30:00Z")).valore,
|
||||||
|
1,
|
||||||
|
"arrivata l'ora dell'evento conta",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
o6Di([pizzataId], new Date("2099-04-15T10:00:00Z")).valore,
|
||||||
0,
|
0,
|
||||||
"lo stesso evento non conta più il mese successivo (si azzera)",
|
"lo stesso evento non conta più il mese successivo (si azzera)",
|
||||||
);
|
);
|
||||||
@@ -386,68 +404,129 @@ if (!locale) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await prova("o7 somma presenze calcolate da eventi/risposte reali del database", async () => {
|
await prova(
|
||||||
// o7 non calcola nulla da `ctx`: somma `g.presenze`, un campo già calcolato a monte da
|
"o7 percentuale di presenze collettive da eventi/risposte reali del database",
|
||||||
// `contaPresenzeGiocatore()` (che in produzione alimenta `useRosa()`). Qui si esercita
|
async () => {
|
||||||
// la stessa funzione pura sui dati appena scritti, per verificare l'intera catena
|
// o7 è la % di presenze su allenamenti e partite definiti (passati e futuri), con i
|
||||||
// DB -> contaPresenzeGiocatore -> o7, non solo la somma finale.
|
// convocati come denominatore. Si esercita l'intera catena DB -> leggiEventi/leggiPresenze -> o7.
|
||||||
const allenamentoId = `${PREFISSO}-o7-allenamento`;
|
const allenamentoId = `${PREFISSO}-o7-allenamento`;
|
||||||
const partitaId = `${PREFISSO}-o7-partita`;
|
const partitaId = `${PREFISSO}-o7-partita`;
|
||||||
const OGGI_STR = "2099-01-20";
|
const [g1, g2, g3] = giocatori;
|
||||||
|
|
||||||
for (const [id, tipo, data] of [
|
for (const [id, tipo, data] of [
|
||||||
[allenamentoId, "allenamento", "2099-01-05"],
|
[allenamentoId, "allenamento", "2099-01-05"],
|
||||||
[partitaId, "partita", "2099-01-08"],
|
[partitaId, "partita", "2099-01-08"],
|
||||||
] as const) {
|
] as const) {
|
||||||
const inserito = await rest("eventi_app", {
|
const inserito = await rest("eventi_app", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id,
|
||||||
|
tipo,
|
||||||
|
titolo: `Test obiettivi o7 (${tipo})`,
|
||||||
|
data,
|
||||||
|
convocati: [g1!.id, g2!.id, g3!.id],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// g1 presente a entrambi, g2 presente e in ritardo (il ritardo conta), g3 assente a
|
||||||
|
// entrambi: 4 presenze su 6 posti (3 convocati x 2 eventi) = 67%.
|
||||||
|
const righe = [
|
||||||
|
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
|
||||||
|
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
|
||||||
|
{ evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
|
||||||
|
{ evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
|
||||||
|
{ evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
|
||||||
|
{ evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
|
||||||
|
];
|
||||||
|
const inserite = await rest("risposte_presenze", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ id, tipo, titolo: `Test obiettivi o7 (${tipo})`, data }),
|
body: JSON.stringify(righe),
|
||||||
});
|
});
|
||||||
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
|
||||||
}
|
|
||||||
|
|
||||||
// g1: presente ai due eventi (2 presenze). g2: presente e in ritardo (2 presenze,
|
const eventiReali = (await leggiEventi()).filter(
|
||||||
// il ritardo conta). g3: assente a entrambi (0 presenze).
|
(e) => e.id === allenamentoId || e.id === partitaId,
|
||||||
const [g1, g2, g3] = giocatori;
|
);
|
||||||
const righe = [
|
const presenzeReali = {
|
||||||
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
|
...(await leggiPresenze(allenamentoId)),
|
||||||
{ evento_id: partitaId, giocatore_id: g1!.id, stato: "presente" },
|
...(await leggiPresenze(partitaId)),
|
||||||
{ evento_id: allenamentoId, giocatore_id: g2!.id, stato: "presente" },
|
};
|
||||||
{ evento_id: partitaId, giocatore_id: g2!.id, stato: "ritardo" },
|
|
||||||
{ evento_id: allenamentoId, giocatore_id: g3!.id, stato: "assente" },
|
|
||||||
{ evento_id: partitaId, giocatore_id: g3!.id, stato: "assente" },
|
|
||||||
];
|
|
||||||
const inserite = await rest("risposte_presenze", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(righe),
|
|
||||||
});
|
|
||||||
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
|
|
||||||
|
|
||||||
const eventiReali = (await leggiEventi()).filter(
|
const o7 = obiettiviSquadra([g1!, g2!, g3!], {
|
||||||
(e) => e.id === allenamentoId || e.id === partitaId,
|
eventi: eventiReali,
|
||||||
);
|
presenze: presenzeReali,
|
||||||
const presenzeReali = {
|
pagelle: [],
|
||||||
...(await leggiPresenze(allenamentoId)),
|
}).find((o) => o.id === "o7")!;
|
||||||
...(await leggiPresenze(partitaId)),
|
|
||||||
};
|
|
||||||
|
|
||||||
const rosaConPresenzeReali = [g1!, g2!, g3!].map((g) => ({
|
assert.equal(
|
||||||
...g,
|
o7.valore,
|
||||||
presenze: contaPresenzeGiocatore(g.id, eventiReali, presenzeReali, OGGI_STR),
|
67,
|
||||||
}));
|
"4 presenze su 6 posti (il ritardo conta), calcolate dal database",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const o7 = obiettiviSquadra(rosaConPresenzeReali, {
|
await prova(
|
||||||
eventi: [],
|
"o1 usa i convocati letti dal database: il non convocato non pesa, l'assente sì, la cena no",
|
||||||
presenze: {},
|
async () => {
|
||||||
pagelle: [],
|
// Stesso mese di test di o1 (marzo 2099). Allenamento con 3 convocati su 14 in rosa:
|
||||||
}).find((o) => o.id === "o7")!;
|
// g1 presente, g2 assente (presenza persa), g3 in ritardo. g4 non è convocato ma risponde
|
||||||
|
// «assente»: non deve pesare. La cena del mese ha risposte «assente» e non deve contare.
|
||||||
|
const allenamentoId = `${PREFISSO}-o1-convocati-allenamento`;
|
||||||
|
const cenaId = `${PREFISSO}-o1-convocati-cena`;
|
||||||
|
const [g1, g2, g3, g4] = giocatori;
|
||||||
|
for (const [id, tipo, data, convocati] of [
|
||||||
|
[allenamentoId, "allenamento", "2099-03-12", [g1!.id, g2!.id, g3!.id]],
|
||||||
|
[cenaId, "evento", "2099-03-14", [g1!.id, g2!.id, g3!.id]],
|
||||||
|
] as const) {
|
||||||
|
const inserito = await rest("eventi_app", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
id,
|
||||||
|
tipo,
|
||||||
|
titolo: `Test o1 convocati (${tipo})`,
|
||||||
|
data,
|
||||||
|
convocati,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
|
||||||
|
}
|
||||||
|
const righe = [
|
||||||
|
{ evento_id: allenamentoId, giocatore_id: g1!.id, stato: "presente" },
|
||||||
|
{ evento_id: allenamentoId, giocatore_id: g2!.id, stato: "assente" },
|
||||||
|
{ evento_id: allenamentoId, giocatore_id: g3!.id, stato: "ritardo" },
|
||||||
|
{ evento_id: allenamentoId, giocatore_id: g4!.id, stato: "assente" },
|
||||||
|
{ evento_id: cenaId, giocatore_id: g1!.id, stato: "assente" },
|
||||||
|
{ evento_id: cenaId, giocatore_id: g2!.id, stato: "assente" },
|
||||||
|
];
|
||||||
|
const inserite = await rest("risposte_presenze", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(righe),
|
||||||
|
});
|
||||||
|
if (!inserite.ok) throw new Error(`inserimento presenze fallito: ${await inserite.text()}`);
|
||||||
|
|
||||||
assert.equal(
|
const ids = [allenamentoId, cenaId];
|
||||||
o7.valore,
|
const eventiReali = (await leggiEventi()).filter((e) => ids.includes(e.id));
|
||||||
4,
|
const presenzeReali = {
|
||||||
"g1 (2) + g2 (2, il ritardo conta) + g3 (0) = 4, calcolate dal database",
|
...(await leggiPresenze(allenamentoId)),
|
||||||
);
|
...(await leggiPresenze(cenaId)),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const o1 = obiettiviSquadra(
|
||||||
|
giocatori,
|
||||||
|
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
|
||||||
|
OGGI,
|
||||||
|
).find((o) => o.id === "o1")!;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
o1.valore,
|
||||||
|
67,
|
||||||
|
"2 presenti (presente e ritardo) su 3 convocati; g4 non convocato e la cena non contano",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await prova("o12/o13 media e conteggio pagelle vere lette da pagelle_voti", async () => {
|
await prova("o12/o13 media e conteggio pagelle vere lette da pagelle_voti", async () => {
|
||||||
const matchId = `${PREFISSO}-o12-m1`;
|
const matchId = `${PREFISSO}-o12-m1`;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/** Check del componente degli avvisi in Home: `bun test/unit/avviso.test.ts`. */
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { createElement } from "react";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { CalendarClock, FileX } from "lucide-react";
|
||||||
|
import { CorpoAvviso, type TonoAvviso } from "@/components/crapp/Avviso";
|
||||||
|
|
||||||
|
const html = (p: Parameters<typeof CorpoAvviso>[0]) =>
|
||||||
|
renderToStaticMarkup(createElement(CorpoAvviso, p));
|
||||||
|
|
||||||
|
// --- ogni tono ha il suo colore, e l'icona è decorativa (il titolo dice già tutto) ---
|
||||||
|
const classi: Record<TonoAvviso, string> = {
|
||||||
|
critico: "bg-destructive",
|
||||||
|
attenzione: "bg-warning",
|
||||||
|
scaduto: "bg-primary",
|
||||||
|
azione: "bg-accent-grad",
|
||||||
|
};
|
||||||
|
for (const [tono, classe] of Object.entries(classi) as [TonoAvviso, string][]) {
|
||||||
|
const h = html({ tono, icona: FileX, titolo: "Titolo" });
|
||||||
|
assert.ok(h.includes(classe), `${tono}: classe colore ${classe}`);
|
||||||
|
assert.ok(h.includes("Titolo"), `${tono}: titolo`);
|
||||||
|
assert.ok(h.includes('aria-hidden="true"'), `${tono}: icona nascosta ai lettori di schermo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- testi: un paragrafo per frase ---------------------------------------------------
|
||||||
|
const conTesti = html({
|
||||||
|
tono: "attenzione",
|
||||||
|
icona: CalendarClock,
|
||||||
|
titolo: "Certificato in scadenza",
|
||||||
|
testi: ["Prima frase.", "Seconda frase."],
|
||||||
|
});
|
||||||
|
assert.equal((conTesti.match(/<p /g) ?? []).length, 3, "titolo + due paragrafi");
|
||||||
|
assert.ok(!conTesti.includes("<ul"), "senza elenco niente lista");
|
||||||
|
|
||||||
|
// --- elenco: righe chi/cosa, conteggio solo con più di una riga ----------------------
|
||||||
|
const una = html({
|
||||||
|
tono: "critico",
|
||||||
|
icona: FileX,
|
||||||
|
titolo: "Certificati mancanti",
|
||||||
|
elenco: [{ chi: "Mario Rossi", cosa: "non caricato" }],
|
||||||
|
});
|
||||||
|
assert.ok(una.includes("Mario Rossi") && una.includes("non caricato"), "riga con chi e cosa");
|
||||||
|
assert.ok(!una.includes("persone"), "una sola riga: nessun conteggio");
|
||||||
|
|
||||||
|
const tre = html({
|
||||||
|
tono: "critico",
|
||||||
|
icona: FileX,
|
||||||
|
titolo: "Certificati mancanti",
|
||||||
|
elenco: [
|
||||||
|
{ chi: "Mario Rossi", cosa: "non caricato" },
|
||||||
|
{ chi: "Luca Bianchi", cosa: "non caricato" },
|
||||||
|
{ chi: "Anna Verdi", cosa: "non caricato" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal((tre.match(/<li /g) ?? []).length, 3, "una riga per persona");
|
||||||
|
assert.ok(tre.includes('aria-label="3 persone"'), "conteggio accessibile");
|
||||||
|
|
||||||
|
// --- freccia solo se la card porta altrove -------------------------------------------
|
||||||
|
const base = { tono: "scaduto", icona: FileX, titolo: "t" } as const;
|
||||||
|
assert.ok(!html(base).includes("lucide-chevron-right"), "senza link niente freccia");
|
||||||
|
assert.ok(
|
||||||
|
html({ ...base, cliccabile: true }).includes("lucide-chevron-right"),
|
||||||
|
"con link la freccia",
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- etichetta e riquadri (turno palloni) --------------------------------------------
|
||||||
|
const palloni = html({
|
||||||
|
tono: "azione",
|
||||||
|
icona: FileX,
|
||||||
|
titolo: "Turno palloni",
|
||||||
|
etichetta: "Oggi",
|
||||||
|
testi: ["Porta i palloni."],
|
||||||
|
riquadri: true,
|
||||||
|
});
|
||||||
|
assert.ok(palloni.includes("Oggi"), "etichetta mostrata");
|
||||||
|
assert.ok(palloni.includes("bg-black/15"), "il messaggio è in un riquadro");
|
||||||
|
assert.ok(palloni.includes("bg-black/25"), "etichetta in pillola");
|
||||||
|
assert.ok(!html({ ...base }).includes("bg-black/25"), "senza etichetta niente pillola");
|
||||||
|
assert.ok(
|
||||||
|
!html({ ...base, testi: ["x"] }).includes("bg-black/15"),
|
||||||
|
"senza riquadri testo semplice",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("avviso: ok");
|
||||||
@@ -69,6 +69,7 @@ const obiettivo = (id: string, valore: number): ObiettivoSquadra => ({
|
|||||||
unita: "%",
|
unita: "%",
|
||||||
emoji: "🎯",
|
emoji: "🎯",
|
||||||
impatto: "",
|
impatto: "",
|
||||||
|
dettaglio: { comeSiCalcola: "", conta: [], nonConta: [], periodo: "", fonte: "", esempio: "" },
|
||||||
});
|
});
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
ids(g(), [], [obiettivo("o1", 95), obiettivo("o2", 92)]).filter((i) => i.startsWith("obiettivo")),
|
ids(g(), [], [obiettivo("o1", 95), obiettivo("o2", 92)]).filter((i) => i.startsWith("obiettivo")),
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/** Check della card di dettaglio degli obiettivi: `bun test/unit/obiettivi-dettaglio.test.ts`. */
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { createElement } from "react";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { SchedaObiettivo } from "@/components/crapp/ObiettivoDrawer";
|
||||||
|
import { Drawer } from "@/components/ui/drawer";
|
||||||
|
import { giocatori } from "@/lib/crapp-data";
|
||||||
|
import { contestoVuoto, obiettiviSquadra, type ObiettivoSquadra } from "@/lib/obiettivi";
|
||||||
|
|
||||||
|
const OGGI = new Date("2026-08-15T10:00:00Z");
|
||||||
|
const tutti = obiettiviSquadra(giocatori, contestoVuoto, OGGI);
|
||||||
|
|
||||||
|
/** Rende la scheda dentro un Drawer aperto, come fa l'app (titolo e descrizione ne hanno bisogno). */
|
||||||
|
const scheda = (o: ObiettivoSquadra) =>
|
||||||
|
renderToStaticMarkup(
|
||||||
|
createElement(Drawer, { open: true }, createElement(SchedaObiettivo, { o })),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- ogni obiettivo ha tutti i testi, nessun campo vuoto ----------------------------
|
||||||
|
const ids = tutti.map((o) => o.id).sort();
|
||||||
|
assert.deepEqual(
|
||||||
|
ids,
|
||||||
|
["o1", "o11", "o12", "o13", "o2", "o3", "o4", "o5", "o6", "o7"],
|
||||||
|
"gli obiettivi sono quelli attesi: se ne aggiungi uno, scrivi il suo dettaglio",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const o of tutti) {
|
||||||
|
const d = o.dettaglio;
|
||||||
|
for (const [nome, testo] of Object.entries({
|
||||||
|
comeSiCalcola: d.comeSiCalcola,
|
||||||
|
periodo: d.periodo,
|
||||||
|
fonte: d.fonte,
|
||||||
|
esempio: d.esempio,
|
||||||
|
})) {
|
||||||
|
assert.ok(testo.trim().length > 20, `${o.id}: «${nome}» troppo corto o vuoto`);
|
||||||
|
}
|
||||||
|
for (const voce of [...d.conta, ...d.nonConta, ...(d.perse ?? [])]) {
|
||||||
|
assert.ok(voce.trim().length > 10, `${o.id}: voce di elenco vuota o troppo corta`);
|
||||||
|
}
|
||||||
|
assert.equal(new Set(d.conta).size, d.conta.length, `${o.id}: voci «conta» duplicate`);
|
||||||
|
assert.equal(new Set(d.nonConta).size, d.nonConta.length, `${o.id}: voci «non conta» duplicate`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- «presenza persa» solo sulle due percentuali di presenze ------------------------
|
||||||
|
for (const o of tutti) {
|
||||||
|
const haPerse = Boolean(o.dettaglio.perse && o.dettaglio.perse.length > 0);
|
||||||
|
assert.equal(haPerse, o.id === "o1" || o.id === "o7", `${o.id}: presenza persa solo su o1 e o7`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- le due presenze spiegano gli stessi punti chiave -------------------------------
|
||||||
|
for (const id of ["o1", "o7"]) {
|
||||||
|
const d = tutti.find((o) => o.id === id)!.dettaglio;
|
||||||
|
const tutto = [d.comeSiCalcola, ...d.conta, ...(d.perse ?? []), ...d.nonConta].join(" ");
|
||||||
|
for (const parola of ["convocat", "presente", "in ritardo", "assente", "cene"]) {
|
||||||
|
assert.ok(tutto.toLowerCase().includes(parola), `${id}: la spiegazione non cita «${parola}»`);
|
||||||
|
}
|
||||||
|
assert.ok(
|
||||||
|
d.nonConta.some((v) => /non è convocato/i.test(v)),
|
||||||
|
`${id}: dice che il non convocato non pesa`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const mese = tutti.find((o) => o.id === "o1")!.dettaglio;
|
||||||
|
const totale = tutti.find((o) => o.id === "o7")!.dettaglio;
|
||||||
|
assert.ok(/mese/i.test(mese.periodo) && /azzera/i.test(mese.periodo), "o1: si azzera ogni mese");
|
||||||
|
assert.ok(/stagione/i.test(totale.periodo) && /mai/i.test(totale.periodo), "o7: non si azzera mai");
|
||||||
|
|
||||||
|
// --- la card renderizzata mostra tutte le sezioni ------------------------------------
|
||||||
|
const o1 = tutti.find((o) => o.id === "o1")!;
|
||||||
|
const html1 = scheda(o1);
|
||||||
|
for (const titolo of [
|
||||||
|
"Stato attuale",
|
||||||
|
"Come si calcola",
|
||||||
|
"Cosa conta",
|
||||||
|
"Presenza persa",
|
||||||
|
"Non entra nel calcolo",
|
||||||
|
"Quando vale",
|
||||||
|
"Da dove arrivano i dati",
|
||||||
|
"Un esempio",
|
||||||
|
]) {
|
||||||
|
assert.ok(html1.includes(titolo), `la card di o1 non mostra «${titolo}»`);
|
||||||
|
}
|
||||||
|
assert.ok(html1.includes(o1.titolo.replace(/&/g, "&")), "titolo");
|
||||||
|
assert.ok(html1.includes(o1.dettaglio.comeSiCalcola), "testo «come si calcola»");
|
||||||
|
assert.ok(html1.includes(o1.impatto), "frase di impatto");
|
||||||
|
assert.ok(html1.includes("entro il"), "o1 ha una scadenza mensile");
|
||||||
|
assert.ok(
|
||||||
|
!html1.includes("Cosa non conta"),
|
||||||
|
"con «presenza persa» la sezione si chiama diversamente",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Un obiettivo senza «presenza persa» ha «Cosa non conta» e nessuna sezione in più.
|
||||||
|
const o12 = tutti.find((o) => o.id === "o12")!;
|
||||||
|
const html12 = scheda(o12);
|
||||||
|
assert.ok(html12.includes("Cosa non conta"), "o12: «Cosa non conta»");
|
||||||
|
assert.ok(!html12.includes("Presenza persa"), "o12: nessuna «presenza persa»");
|
||||||
|
assert.ok(!html12.includes("Non entra nel calcolo"), "o12: nessun «non entra nel calcolo»");
|
||||||
|
assert.ok(!html12.includes("entro il"), "o12 non ha scadenza");
|
||||||
|
|
||||||
|
// --- stato: in corso / completato, e i numeri mostrati --------------------------------
|
||||||
|
const finto = (valore: number): ObiettivoSquadra => ({ ...o12, valore, target: 10 });
|
||||||
|
assert.ok(scheda(finto(4)).includes("In corso"), "sotto il target: in corso");
|
||||||
|
assert.ok(scheda(finto(4)).includes("40%"), "percentuale mostrata");
|
||||||
|
assert.ok(scheda(finto(10)).includes("Completato"), "al target: completato");
|
||||||
|
assert.ok(!scheda(finto(10)).includes("In corso"), "al target non è più in corso");
|
||||||
|
|
||||||
|
console.log("obiettivi-dettaglio: ok");
|
||||||
+335
-17
@@ -100,6 +100,169 @@ const soloCompleanni: ContestoObiettivi = {
|
|||||||
};
|
};
|
||||||
assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2").valore, 0);
|
assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2").valore, 0);
|
||||||
|
|
||||||
|
// --- o1: chi non è convocato non occupa un posto, la sua assenza non pesa -------
|
||||||
|
{
|
||||||
|
const [g1, g2] = giocatori;
|
||||||
|
const mese: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [{ ...evento("m1", "2026-08-10", "allenamento"), convocati: [g1!.id, g2!.id] }],
|
||||||
|
presenze: { m1: { [g1!.id]: "presente", [g2!.id]: "assente" } },
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, mese, OGGI_AGOSTO), "o1").valore,
|
||||||
|
50,
|
||||||
|
"su 2 convocati 1 presente e 1 assente: 50%, il resto della rosa non conta",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(
|
||||||
|
obiettiviSquadra(
|
||||||
|
giocatori,
|
||||||
|
{ ...mese, eventi: [evento("m1", "2026-08-10", "allenamento")] },
|
||||||
|
OGGI_AGOSTO,
|
||||||
|
),
|
||||||
|
"o1",
|
||||||
|
).valore,
|
||||||
|
Math.round((1 / giocatori.length) * 100),
|
||||||
|
"senza convocati i posti sono tutta la rosa",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- presenze (o1 mese, o7 totale): casi limite del calcolo ------------------------
|
||||||
|
{
|
||||||
|
const [g1, g2, g3, g4] = giocatori;
|
||||||
|
const P = (...x: [string, "presente" | "ritardo" | "assente" | "forse" | "infortunato"][]) =>
|
||||||
|
Object.fromEntries(x);
|
||||||
|
const o1v = (c: ContestoObiettivi, oggi = OGGI_AGOSTO) =>
|
||||||
|
trova(obiettiviSquadra(giocatori, c, oggi), "o1").valore;
|
||||||
|
const o7v = (c: ContestoObiettivi) => trova(obiettiviSquadra(giocatori, c), "o7").valore;
|
||||||
|
const conv = (id: string, data: string, tipo: Evento["tipo"], convocati: string[]) => ({
|
||||||
|
...evento(id, data, tipo),
|
||||||
|
convocati,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Solo allenamenti e partite entrano nel conto: cene, eventi e compleanni restano fuori
|
||||||
|
// anche se hanno risposte di presenza.
|
||||||
|
const conAltri: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [
|
||||||
|
conv("t1", "2026-08-05", "allenamento", [g1!.id, g2!.id]),
|
||||||
|
conv("t2", "2026-08-06", "evento", [g1!.id, g2!.id]),
|
||||||
|
conv("t3", "2026-08-07", "compleanno", [g1!.id, g2!.id]),
|
||||||
|
],
|
||||||
|
presenze: {
|
||||||
|
t1: P([g1!.id, "presente"], [g2!.id, "assente"]),
|
||||||
|
t2: P([g1!.id, "assente"], [g2!.id, "assente"]),
|
||||||
|
t3: P([g1!.id, "presente"], [g2!.id, "presente"]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.equal(o1v(conAltri), 50, "o1: solo l'allenamento conta (1 su 2)");
|
||||||
|
assert.equal(o7v(conAltri), 50, "o7: solo l'allenamento conta (1 su 2)");
|
||||||
|
|
||||||
|
// Assente, forse, infortunato e nessuna risposta sono tutti presenze perse: posto vuoto.
|
||||||
|
const perse: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [conv("p1", "2026-08-05", "partita", [g1!.id, g2!.id, g3!.id, g4!.id])],
|
||||||
|
presenze: { p1: P([g1!.id, "assente"], [g2!.id, "forse"], [g3!.id, "infortunato"]) },
|
||||||
|
};
|
||||||
|
assert.equal(o1v(perse), 0, "o1: nessuna presenza, 0 su 4");
|
||||||
|
assert.equal(o7v(perse), 0, "o7: nessuna presenza, 0 su 4");
|
||||||
|
const unaPresente = {
|
||||||
|
...perse,
|
||||||
|
presenze: { p1: P([g1!.id, "presente"], [g2!.id, "forse"], [g3!.id, "infortunato"]) },
|
||||||
|
};
|
||||||
|
assert.equal(o1v(unaPresente), 25, "o1: 1 presente su 4 convocati");
|
||||||
|
assert.equal(o7v(unaPresente), 25, "o7: 1 presente su 4 convocati");
|
||||||
|
|
||||||
|
// Un non convocato che risponde non gonfia la percentuale, e se è assente non la abbassa.
|
||||||
|
const nonConv: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [conv("n1", "2026-08-05", "allenamento", [g1!.id])],
|
||||||
|
presenze: { n1: P([g1!.id, "presente"], [g2!.id, "assente"], [g3!.id, "presente"]) },
|
||||||
|
};
|
||||||
|
assert.equal(o1v(nonConv), 100, "o1: l'assente non convocato non pesa, il presente nemmeno");
|
||||||
|
assert.equal(o7v(nonConv), 100, "o7: idem");
|
||||||
|
|
||||||
|
// Il mese e il totale coincidono quando tutti gli eventi sono del mese corrente;
|
||||||
|
// il totale invece comprende anche gli altri mesi.
|
||||||
|
const dueMesi: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [
|
||||||
|
conv("m1", "2026-08-05", "allenamento", [g1!.id, g2!.id]),
|
||||||
|
conv("m2", "2026-07-05", "allenamento", [g1!.id, g2!.id]),
|
||||||
|
],
|
||||||
|
presenze: {
|
||||||
|
m1: P([g1!.id, "presente"], [g2!.id, "presente"]),
|
||||||
|
m2: P([g1!.id, "assente"], [g2!.id, "assente"]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.equal(o1v(dueMesi), 100, "o1: guarda solo agosto");
|
||||||
|
assert.equal(o7v(dueMesi), 50, "o7: guarda agosto e luglio insieme");
|
||||||
|
assert.equal(o1v(dueMesi, new Date("2026-07-15T10:00:00Z")), 0, "o1 a luglio: solo luglio");
|
||||||
|
const soloAgosto = { ...dueMesi, eventi: [dueMesi.eventi[0]!] };
|
||||||
|
assert.equal(o1v(soloAgosto), o7v(soloAgosto), "stessi eventi: stessa percentuale");
|
||||||
|
|
||||||
|
// Confine di mese nel fuso Europe/Rome: il 30 giugno alle 22:30 UTC a Roma è già luglio.
|
||||||
|
const sera = new Date("2026-06-30T22:30:00Z");
|
||||||
|
const luglio: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [conv("l1", "2026-07-01", "allenamento", [g1!.id])],
|
||||||
|
presenze: { l1: P([g1!.id, "presente"]) },
|
||||||
|
};
|
||||||
|
assert.equal(o1v(luglio, sera), 100, "a Roma è già il 1° luglio: l'evento di luglio conta");
|
||||||
|
const giugno = { ...luglio, eventi: [conv("l1", "2026-06-30", "allenamento", [g1!.id])] };
|
||||||
|
assert.equal(o1v(giugno, sera), 0, "a Roma giugno è finito: l'evento del 30 giugno non conta");
|
||||||
|
|
||||||
|
// Convocato sconosciuto (non in rosa): nessun posto, quindi 0 e non NaN.
|
||||||
|
const fantasma: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [conv("f1", "2026-08-05", "allenamento", ["fantasma"])],
|
||||||
|
presenze: { f1: P(["fantasma", "presente"]) },
|
||||||
|
};
|
||||||
|
assert.equal(o1v(fantasma), 0, "o1: convocato fuori rosa, 0 posti");
|
||||||
|
assert.equal(o7v(fantasma), 0, "o7: convocato fuori rosa, 0 posti");
|
||||||
|
|
||||||
|
// Evento senza nessuna risposta: i posti ci sono, le presenze no.
|
||||||
|
const senzaRisposte: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [conv("r1", "2026-08-05", "allenamento", [g1!.id, g2!.id])],
|
||||||
|
};
|
||||||
|
assert.equal(o1v(senzaRisposte), 0, "o1: nessuna risposta = 0%");
|
||||||
|
|
||||||
|
// Rosa vuota: nessuna divisione per zero.
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra([], conAltri, OGGI_AGOSTO), "o1").valore,
|
||||||
|
0,
|
||||||
|
"rosa vuota: o1",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra([], conAltri, OGGI_AGOSTO), "o7").valore,
|
||||||
|
0,
|
||||||
|
"rosa vuota: o7",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Arrotondamento: 2 su 3 = 67%, e il progresso non supera mai il 100%.
|
||||||
|
const treConvocati: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [conv("a3", "2026-08-05", "allenamento", [g1!.id, g2!.id, g3!.id])],
|
||||||
|
presenze: { a3: P([g1!.id, "presente"], [g2!.id, "ritardo"], [g3!.id, "assente"]) },
|
||||||
|
};
|
||||||
|
assert.equal(o1v(treConvocati), 67, "o1: 2 su 3 arrotondato");
|
||||||
|
assert.equal(o7v(treConvocati), 67, "o7: 2 su 3 arrotondato");
|
||||||
|
|
||||||
|
// Soglia del 90%: la percentuale arriva al target e l'obiettivo risulta completato.
|
||||||
|
const dieci = giocatori.slice(0, 10).map((g) => g.id);
|
||||||
|
const novePresenti: ContestoObiettivi = {
|
||||||
|
...contestoVuoto,
|
||||||
|
eventi: [conv("d1", "2026-08-05", "allenamento", dieci)],
|
||||||
|
presenze: {
|
||||||
|
d1: Object.fromEntries(dieci.map((id, i) => [id, i === 0 ? "assente" : "presente"])),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const ob1 = trova(obiettiviSquadra(giocatori, novePresenti, OGGI_AGOSTO), "o1");
|
||||||
|
assert.equal(ob1.valore, 90, "9 su 10 = 90%");
|
||||||
|
assert.equal(progressoObiettivo(ob1), 100, "90% su target 90% = completato");
|
||||||
|
}
|
||||||
|
|
||||||
// --- o1: si azzera a ogni cambio mese, in base agli eventi a calendario ------
|
// --- o1: si azzera a ogni cambio mese, in base agli eventi a calendario ------
|
||||||
{
|
{
|
||||||
// Stesso evento/presenze: "in mese" a settembre, "fuori mese" se letto da agosto.
|
// Stesso evento/presenze: "in mese" a settembre, "fuori mese" se letto da agosto.
|
||||||
@@ -216,7 +379,7 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
|
|||||||
|
|
||||||
// --- o6: evento di squadra al mese, si azzera come o1 ------------------------
|
// --- o6: evento di squadra al mese, si azzera come o1 ------------------------
|
||||||
{
|
{
|
||||||
const OGGI_SETTEMBRE = new Date("2026-09-05T10:00:00Z");
|
const OGGI_SETTEMBRE = new Date("2026-09-20T10:00:00Z");
|
||||||
const pizzataSettembre: ContestoObiettivi = {
|
const pizzataSettembre: ContestoObiettivi = {
|
||||||
eventi: [evento("p1", "2026-09-12", "evento")],
|
eventi: [evento("p1", "2026-09-12", "evento")],
|
||||||
presenze: {},
|
presenze: {},
|
||||||
@@ -233,6 +396,85 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
|
|||||||
"lo stesso evento non conta se oggi è agosto",
|
"lo stesso evento non conta se oggi è agosto",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Definito ma non ancora arrivato: non conta finché non scatta l'ora dell'evento.
|
||||||
|
const pizzataFutura: ContestoObiettivi = {
|
||||||
|
eventi: [{ ...evento("pf1", "2026-09-28", "evento"), ora: "20:30" }],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, pizzataFutura, OGGI_SETTEMBRE), "o6").valore,
|
||||||
|
0,
|
||||||
|
"un evento futuro non completa l'obiettivo al momento della definizione",
|
||||||
|
);
|
||||||
|
const serata = { ...evento("ps1", "2026-09-20", "evento"), ora: "20:30" };
|
||||||
|
const conSerata: ContestoObiettivi = { eventi: [serata], presenze: {}, pagelle: [] };
|
||||||
|
// 20/09 10:00Z = 12:00 a Roma (CEST): prima delle 20:30 non conta, dopo sì.
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, conSerata, OGGI_SETTEMBRE), "o6").valore,
|
||||||
|
0,
|
||||||
|
"stesso giorno ma ora non ancora arrivata: non conta",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, conSerata, new Date("2026-09-20T18:30:00Z")), "o6").valore,
|
||||||
|
1,
|
||||||
|
"arrivata l'ora dell'evento (20:30 a Roma): conta",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Senza ora vale 00:00: conta dal giorno stesso, non il giorno prima.
|
||||||
|
const senzaOra: ContestoObiettivi = {
|
||||||
|
eventi: [{ ...evento("so1", "2026-09-20", "evento"), ora: "" }],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, senzaOra, OGGI_SETTEMBRE), "o6").valore,
|
||||||
|
1,
|
||||||
|
"evento senza ora: conta dal giorno stesso",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, senzaOra, new Date("2026-09-19T21:00:00Z")), "o6").valore,
|
||||||
|
0,
|
||||||
|
"evento senza ora: la sera prima (Roma) non conta",
|
||||||
|
);
|
||||||
|
// Ora legale: il 20/09 vale CEST (UTC+2), il 20/12 CET (UTC+1): 20:30 Roma = 18:30Z / 19:30Z.
|
||||||
|
const inverno = { ...evento("iv1", "2026-12-20", "evento"), ora: "20:30" };
|
||||||
|
const ctxInverno: ContestoObiettivi = { eventi: [inverno], presenze: {}, pagelle: [] };
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxInverno, new Date("2026-12-20T19:29:00Z")), "o6").valore,
|
||||||
|
0,
|
||||||
|
"inverno: 19:29Z = 20:29 a Roma, non ancora",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxInverno, new Date("2026-12-20T19:30:00Z")), "o6").valore,
|
||||||
|
1,
|
||||||
|
"inverno: 19:30Z = 20:30 a Roma, conta",
|
||||||
|
);
|
||||||
|
// Cambio mese a mezzanotte di Roma: 30/09 23:30Z è già 01/10 a Roma (CEST).
|
||||||
|
const fineSett = { ...evento("fs1", "2026-09-30", "evento"), ora: "20:00" };
|
||||||
|
const ctxFine: ContestoObiettivi = { eventi: [fineSett], presenze: {}, pagelle: [] };
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxFine, new Date("2026-09-30T21:59:00Z")), "o6").valore,
|
||||||
|
1,
|
||||||
|
"30/09 23:59 a Roma: ancora settembre, conta",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, ctxFine, new Date("2026-09-30T22:00:00Z")), "o6").valore,
|
||||||
|
0,
|
||||||
|
"01/10 00:00 a Roma: nuovo mese, si azzera",
|
||||||
|
);
|
||||||
|
// Più eventi: ne basta uno passato; quello futuro non si somma.
|
||||||
|
const misti: ContestoObiettivi = {
|
||||||
|
eventi: [evento("m1", "2026-09-05", "evento"), evento("m2", "2026-09-28", "evento")],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
trova(obiettiviSquadra(giocatori, misti, OGGI_SETTEMBRE), "o6").valore,
|
||||||
|
1,
|
||||||
|
"conta solo l'evento già passato",
|
||||||
|
);
|
||||||
|
|
||||||
const allenamentoNelMese: ContestoObiettivi = {
|
const allenamentoNelMese: ContestoObiettivi = {
|
||||||
eventi: [evento("al1", "2026-08-12", "allenamento")],
|
eventi: [evento("al1", "2026-08-12", "allenamento")],
|
||||||
presenze: {},
|
presenze: {},
|
||||||
@@ -245,23 +487,85 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- somme sulla rosa --------------------------------------------------------
|
// --- o7: presenze collettive (% su allenamenti e partite definiti, passati e futuri) ---
|
||||||
const sommaPresenze = giocatori.reduce((s, g) => s + g.presenze, 0);
|
{
|
||||||
assert.equal(trova(vuoti, "o7").valore, sommaPresenze);
|
const [g1, g2, g3, g4] = giocatori;
|
||||||
assert.equal(
|
const rosa4 = [g1!, g2!, g3!, g4!];
|
||||||
trova(obiettiviSquadra([], contestoVuoto), "o7").valore,
|
const o7 = (c: ContestoObiettivi, r = rosa4) => trova(obiettiviSquadra(r, c, OGGI_AGOSTO), "o7");
|
||||||
0,
|
|
||||||
"rosa vuota: nessuna presenza",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Valori noti e indipendenti dai dati reali della rosa: non solo la stessa formula
|
assert.equal(o7(contestoVuoto).titolo, "Presenze collettive");
|
||||||
// ricalcolata sugli stessi dati, ma una somma verificabile a mente (5 + 10 + 15 = 30).
|
assert.equal(o7(contestoVuoto).target, 90);
|
||||||
const rosaControllata = giocatori.slice(0, 3).map((g, i) => ({ ...g, presenze: [5, 10, 15][i]! }));
|
assert.equal(o7(contestoVuoto).valore, 0, "nessun evento: 0, non NaN");
|
||||||
assert.equal(
|
assert.equal(o7({ ...contestoVuoto }, []).valore, 0, "rosa vuota: 0");
|
||||||
trova(obiettiviSquadra(rosaControllata, contestoVuoto), "o7").valore,
|
|
||||||
30,
|
// Allenamento passato (3 presenti su 4) + partita futura (1 presente su 4): 4 su 8 = 50%.
|
||||||
"somma di presenze note, indipendente dal roster reale",
|
const base: ContestoObiettivi = {
|
||||||
);
|
eventi: [evento("c1", "2026-07-01", "allenamento"), evento("c2", "2026-12-01", "partita")],
|
||||||
|
presenze: {
|
||||||
|
c1: { [g1!.id]: "presente", [g2!.id]: "ritardo", [g3!.id]: "presente", [g4!.id]: "assente" },
|
||||||
|
c2: { [g1!.id]: "presente", [g2!.id]: "assente" },
|
||||||
|
},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(o7(base).valore, 50, "passati e futuri nel denominatore, ritardo conta");
|
||||||
|
|
||||||
|
// Con convocati il denominatore sono solo loro: c1 per g1 e g2 (2 presenti su 2) = 100%.
|
||||||
|
const conConvocati: ContestoObiettivi = {
|
||||||
|
...base,
|
||||||
|
eventi: [{ ...evento("c1", "2026-07-01", "allenamento"), convocati: [g1!.id, g2!.id] }],
|
||||||
|
};
|
||||||
|
assert.equal(o7(conConvocati).valore, 100, "denominatore = convocati");
|
||||||
|
|
||||||
|
// Un non convocato che risponde "presente" non gonfia la percentuale.
|
||||||
|
const nonConvocato: ContestoObiettivi = {
|
||||||
|
...conConvocati,
|
||||||
|
presenze: { c1: { [g1!.id]: "presente", [g3!.id]: "presente" } },
|
||||||
|
};
|
||||||
|
assert.equal(o7(nonConvocato).valore, 50, "i non convocati non contano (1 su 2)");
|
||||||
|
|
||||||
|
// Arrotondamento: 179 presenze su 200 posti = 89,5% -> 90; 178 su 200 = 89%.
|
||||||
|
const rosa100 = Array.from({ length: 100 }, (_, i) => ({ ...g1!, id: `r${i}` }));
|
||||||
|
const rispondi = (presenti: number) =>
|
||||||
|
Object.fromEntries(rosa100.map((g, i) => [g.id, i < presenti ? "presente" : "assente"]));
|
||||||
|
const duePartite = (a: number, b: number): ContestoObiettivi => ({
|
||||||
|
eventi: [evento("t1", "2026-07-01", "allenamento"), evento("t2", "2026-07-02", "allenamento")],
|
||||||
|
presenze: {
|
||||||
|
t1: rispondi(a) as Record<string, "presente" | "assente">,
|
||||||
|
t2: rispondi(b) as Record<string, "presente" | "assente">,
|
||||||
|
},
|
||||||
|
pagelle: [],
|
||||||
|
});
|
||||||
|
assert.equal(o7(duePartite(90, 89), rosa100).valore, 90, "89,5% arrotonda a 90");
|
||||||
|
assert.equal(o7(duePartite(90, 88), rosa100).valore, 89, "89% resta 89");
|
||||||
|
assert.equal(o7(duePartite(100, 100), rosa100).valore, 100);
|
||||||
|
|
||||||
|
// Evento privo di risposte: pesa nel denominatore ma non nel numeratore.
|
||||||
|
const senzaRisposte: ContestoObiettivi = {
|
||||||
|
eventi: [evento("n1", "2026-07-01", "allenamento")],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(o7(senzaRisposte).valore, 0, "nessuna risposta: 0%");
|
||||||
|
|
||||||
|
// Convocati che non sono in rosa: non entrano nei posti.
|
||||||
|
const fuoriRosa: ContestoObiettivi = {
|
||||||
|
eventi: [{ ...evento("f1", "2026-07-01", "allenamento"), convocati: ["sconosciuto"] }],
|
||||||
|
presenze: {},
|
||||||
|
pagelle: [],
|
||||||
|
};
|
||||||
|
assert.equal(o7(fuoriRosa).valore, 0, "nessun posto valido: 0, non NaN");
|
||||||
|
|
||||||
|
// Eventi sociali e compleanni non fanno parte del denominatore.
|
||||||
|
const sociali: ContestoObiettivi = {
|
||||||
|
...base,
|
||||||
|
eventi: [
|
||||||
|
...base.eventi,
|
||||||
|
evento("c3", "2026-07-10", "evento"),
|
||||||
|
evento("c4", "2026-07-11", "compleanno"),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert.equal(o7(sociali).valore, 50, "solo allenamenti e partite");
|
||||||
|
}
|
||||||
|
|
||||||
const pagelle: VotoPagella[] = [
|
const pagelle: VotoPagella[] = [
|
||||||
{ match_id: "m1", votante_id: "g1", votato_id: "g2", voto: 7 },
|
{ match_id: "m1", votante_id: "g1", votato_id: "g2", voto: 7 },
|
||||||
@@ -388,6 +692,7 @@ const o = (valore: number, target: number): ObiettivoSquadra => ({
|
|||||||
unita: "%",
|
unita: "%",
|
||||||
emoji: "🎯",
|
emoji: "🎯",
|
||||||
impatto: "i",
|
impatto: "i",
|
||||||
|
dettaglio: { comeSiCalcola: "", conta: [], nonConta: [], periodo: "", fonte: "", esempio: "" },
|
||||||
});
|
});
|
||||||
assert.equal(progressoObiettivo(o(0, 10)), 0);
|
assert.equal(progressoObiettivo(o(0, 10)), 0);
|
||||||
assert.equal(progressoObiettivo(o(5, 10)), 50);
|
assert.equal(progressoObiettivo(o(5, 10)), 50);
|
||||||
@@ -417,4 +722,17 @@ assert.equal(microcopyObiettivo(o(60, 100)), "Oltre metà strada: ancora 40 %.")
|
|||||||
assert.equal(microcopyObiettivo(o(10, 100)), "Si parte: 90 % al traguardo.");
|
assert.equal(microcopyObiettivo(o(10, 100)), "Si parte: 90 % al traguardo.");
|
||||||
assert.equal(microcopyObiettivo(o(0, 100)), "Tocca a noi far partire questo obiettivo.");
|
assert.equal(microcopyObiettivo(o(0, 100)), "Tocca a noi far partire questo obiettivo.");
|
||||||
|
|
||||||
|
// --- dettaglio: ogni obiettivo spiega tutto, senza campi vuoti ---------------------
|
||||||
|
for (const ob of obiettiviSquadra(giocatori, contestoVuoto)) {
|
||||||
|
const d = ob.dettaglio;
|
||||||
|
for (const campo of [d.comeSiCalcola, d.periodo, d.fonte, d.esempio]) {
|
||||||
|
assert.ok(campo.trim().length > 0, `${ob.id}: testo di dettaglio vuoto`);
|
||||||
|
}
|
||||||
|
assert.ok(d.conta.length > 0, `${ob.id}: manca «cosa conta»`);
|
||||||
|
assert.ok(d.nonConta.length > 0, `${ob.id}: manca «cosa non conta»`);
|
||||||
|
if (ob.id === "o1" || ob.id === "o7") {
|
||||||
|
assert.ok(d.perse && d.perse.length > 0, `${ob.id}: manca «presenza persa»`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("obiettivi: ok");
|
console.log("obiettivi: ok");
|
||||||
Reference in new issue
Block a user