diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 052aefd..b6f54fb 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,6 +1,6 @@ # Project State -Ultimo aggiornamento: 01/10/2026 +Ultimo aggiornamento: 06/10/2026 Questo file dice **a che punto siamo ora**. Le funzionalità stanno in [docs/ROADMAP.md](docs/ROADMAP.md), i rilasci in [docs/CHANGELOG.md](docs/CHANGELOG.md), lo @@ -9,17 +9,18 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in ## Stato generale -- **Versione:** 1.2.3 (in produzione su `main` la 1.2.2) — notifiche su tre canali, avvisi di palloni - (3 ore prima) e presenze (24 ore prima) una sola volta, niente promemoria evento, niente foto - tessera, avviso certificato a 30 giorni per il giocatore, avviso rosso per chi non l'ha caricato. +- **Versione:** 1.3.0 (tag `v1.3.0` su `main`) — notifica di compleanno, obiettivi cliccabili con card di + dettaglio, presenze calcolate sui convocati, avvisi in Home ridisegnati, caratteri più grandi, tessera CSI + tolta dalla dashboard. Dettaglio in [docs/CHANGELOG.md](docs/CHANGELOG.md). - **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio Project Ref `hetycilxgkdmccelwerq` non si usano più. -- **Migration:** 41 file in `supabase/migrations/`, fino a `m30_sollecito_solo_24h`, tutte - applicate in produzione (verificato con `npx supabase migration list` il 01/10/2026). La `m29` toglie +- **Migration:** 42 file in `supabase/migrations/`, fino a `m31_notifica_compleanno`. Le prime 41 (fino a + `m30_sollecito_solo_24h`) sono applicate in produzione (verificato con `npx supabase migration list` il + 01/10/2026); la `m31` no, ancora da applicare (DD-047). La `m29` toglie `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 ([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 solo da `user_roles`. Il collegamento degli account è un processo continuo: ogni giocatore si collega al primo accesso (DD-018). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1a74348..febec84 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -85,9 +85,13 @@ quando il database non risponde. ## UI 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. +**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 `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 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2a30359..b577df6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,49 @@ versioni `0.y.z` erano pre-release. ## [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 + +- **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 + 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. +- **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 Nella Home si vede anche chi non ha caricato il certificato medico, e le dipendenze sono aggiornate perché Vercel bloccava il deploy. Nessuna migration. diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 3779090..cd6de2f 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -22,6 +22,7 @@ sezione «Lettura» qui sotto; `anon` non arriva a nessuna di esse da M4 (DD-011 | `notifiche_email_coda` | nessuno da client, né in lettura né in scrittura: la riempie il trigger `accoda_notifica_email` e la gestisce solo il worker email con la service role (M22) | | `preferenze_utente` | l'utente sulla propria riga (`auth_user_id = auth.uid()`), in lettura e scrittura (M22) | | `promemoria_eventi_generati` | nessuno da client, né in lettura né in scrittura: la scrive solo la funzione `genera_promemoria_eventi()` dei cron (M23) | +| `compleanni_notificati` | Registro degli avvisi di compleanno già generati, per destinatario e giorno. | Chiave `(giocatore_id, giorno)`, `ON DELETE CASCADE` verso il giocatore (DD-029, DD-047, M31). Non è legato a un evento. Eliminare la notifica non fa rigenerare l'avviso. | | `notifiche_push_coda` | nessuno da client, né in lettura né in scrittura: la riempie il trigger `accoda_push_promemoria` e la gestisce solo il worker con la service role (M24) | | `profili_giocatore` | il giocatore sul proprio profilo, admin su tutti (DD-016, DD-017) | | `giocatori_squadra` | admin; il giocatore può solo reclamare uno slot libero (DD-016); l'allenatore anche cambiare nome e cognome del proprio slot (M21) | @@ -58,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`. | | `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). | | `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. | @@ -121,11 +122,11 @@ votante e votato convocati all'evento (RLS, `evento_permette_voto()`, DD-027). - Non c'è una notifica alla sola creazione di un evento: conta l'avvicinarsi della data. - Destinatari di un evento (`giocatori_destinatari_evento()`): giocatori attivi, i convocati o tutta la rosa se `convocati` è vuoto; mai gli allenatori (DD-039). -- Il `CHECK` sul tipo ammette anche `turno_palloni_12h`, `_6h`, `_3h`, `turno_palloni_revocato` e +- Il `CHECK` sul tipo ammette anche `compleanno` e `compleanno_auguri` (M31, DD-047), `turno_palloni_12h`, `_6h`, `_3h`, `turno_palloni_revocato` e `sollecita_presenze_24h`, `_12h`, `_6h`, scritti dai job (DD-040). `turno_palloni` e `sollecita_presenze` restano i tipi dei pulsanti manuali. - Il trigger delle email salta tutti i tipi `turno_palloni*`; quello delle push accoda i tipi - automatici ma non quelli dei pulsanti. + automatici (compleanni compresi) ma non quelli dei pulsanti. ### Code di invio (`notifiche_email_coda`, `notifiche_push_coda`) @@ -151,15 +152,16 @@ Le notifiche automatiche nascono nel database, da job `pg_cron` che chiamano fun riservate alla service role (anon e utenti autenticati non possono chiamarle). Testi e regole sono in [modules/notifiche.md](modules/notifiche.md); le decisioni in DD-037, DD-038 e DD-040. -| Funzione (migration) | Cosa fa | Job `pg_cron` | -| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -| `genera_promemoria_eventi(tipo, finestra)` (M17, M23, M26) | Promemoria a 24 e 3 ore, con «oggi/domani», data, ora e luogo. **Non pianificata dalla M28** (DD-043): resta come funzione, nessun job la chiama. | `promemoria-eventi-24h` (ogni ora), `-3h` (15 min) | -| `genera_avvisi_palloni()` (M26, M27), con `genera_avvisi_palloni_fascia()` | Avviso del turno palloni a 3 ore (M27; in M26 erano 12, 6 e 3) per gli eventi con un incaricato confermato; poi chiama `genera_revoche_palloni()`. | `avvisi-palloni-automatici` (15 min) | -| `genera_revoche_palloni()` (M26) | Una revoca per chi aveva un avviso automatico e non è più destinatario, per eventi non ancora iniziati; guarda il registro. | (dentro `genera_avvisi_palloni()`) | -| `genera_solleciti_presenze()` (M26, M30), con `..._fascia()` | Sollecito a 24 ore (M30; in M26 erano 24, 12 e 6) ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) | -| `inizio_evento(data, ora)` (M26) | Istante d'inizio di un evento in fuso `Europe/Rome`; `NULL` se l'ora non è valida, e l'evento viene solo escluso. | — | -| `giocatori_destinatari_evento(evento)` (M17, M21, M25) | Giocatori attivi destinatari: i convocati, o tutta la rosa se `convocati` è vuoto; mai gli allenatori. | — | -| `accoda_notifica_email()`, `accoda_push_promemoria()` (M22, M24, M26) | Trigger su `notifiche_utente`: la mail salta i tipi `turno_palloni*`; la push accoda i tipi automatici (non quelli dei pulsanti). | — | +| Funzione (migration) | Cosa fa | Job `pg_cron` | +| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| `genera_promemoria_eventi(tipo, finestra)` (M17, M23, M26) | Promemoria a 24 e 3 ore, con «oggi/domani», data, ora e luogo. **Non pianificata dalla M28** (DD-043): resta come funzione, nessun job la chiama. | `promemoria-eventi-24h` (ogni ora), `-3h` (15 min) | +| `genera_avvisi_palloni()` (M26, M27), con `genera_avvisi_palloni_fascia()` | Avviso del turno palloni a 3 ore (M27; in M26 erano 12, 6 e 3) per gli eventi con un incaricato confermato; poi chiama `genera_revoche_palloni()`. | `avvisi-palloni-automatici` (15 min) | +| `genera_revoche_palloni()` (M26) | Una revoca per chi aveva un avviso automatico e non è più destinatario, per eventi non ancora iniziati; guarda il registro. | (dentro `genera_avvisi_palloni()`) | +| `genera_solleciti_presenze()` (M26, M30), con `..._fascia()` | Sollecito a 24 ore (M30; in M26 erano 24, 12 e 6) ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) | +| `genera_avvisi_compleanno(adesso)` (M31) | Dalle 8:00 (`Europe/Rome`) manda gli auguri a chi compie gli anni e l'avviso (solo titolo, senza età) agli altri membri attivi, un messaggio a testa; guarda il registro `compleanni_notificati`. `adesso` (default `now()`) serve ai test. 29 febbraio non gestito (DD-047). | `avvisi-compleanno` (ogni ora) | +| `inizio_evento(data, ora)` (M26) | Istante d'inizio di un evento in fuso `Europe/Rome`; `NULL` se l'ora non è valida, e l'evento viene solo escluso. | — | +| `giocatori_destinatari_evento(evento)` (M17, M21, M25) | Giocatori attivi destinatari: i convocati, o tutta la rosa se `convocati` è vuoto; mai gli allenatori. | — | +| `accoda_notifica_email()`, `accoda_push_promemoria()` (M22, M24, M26) | Trigger su `notifiche_utente`: la mail salta i tipi `turno_palloni*`; la push accoda i tipi automatici (non quelli dei pulsanti). | — | ## Badge diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d609f27..eacf5f4 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 (caricamento, scadenza, stato, download); la foto tessera è stata tolta (DD-044); lo storico dei certificati resta un'estensione futura -- [x] Gestione tesseramenti CSI — raccolta dati, export CSV e tracciamento di chi è già - tesserato (numero e data di tessera) +- [x] Gestione tesseramenti CSI — raccolta dati ed export CSV; il tracciamento di numero e data + di tessera è stato tolto dalla dashboard (DD-048) - [x] Collegamento CSI (stagione 2025/26) - [x] Classifica automatica (campionato e Coppa) - [x] Risultati campionato @@ -47,6 +47,9 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). - [x] Notifiche automatiche — turno palloni 3 ore prima, sollecito presenze 24 ore prima (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano; il promemoria a 24 e 3 ore è stato tolto, DD-043) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040) +- [x] Notifica di compleanno — auguri al festeggiato e avviso a tutti gli altri, ogni giorno dalle 8:00, su push, + centro notifiche ed email ([specifica](modules/notifiche.md#catalogo-delle-notifiche), DD-047) + ## Prossimo - [ ] Calendario ufficiale — i dati delle gare future arrivano già dal feed CSI, la pagina diff --git a/docs/decisions/DD-047.md b/docs/decisions/DD-047.md new file mode 100644 index 0000000..8678cc3 --- /dev/null +++ b/docs/decisions/DD-047.md @@ -0,0 +1,65 @@ +# DD-047 — Notifica di compleanno + +**Data:** 6 ottobre 2026 +**Stato:** Accettata (migration `m31`, non ancora applicata in produzione; nasce dalla issue #11 «notifica») + +**Contesto** +I compleanni si vedono solo aprendo il Calendario, dove `compleanniEventi()` li calcola dalla data di +nascita pubblica (`giocatori_squadra.nascita`, DD-031). Nessun avviso arriva ad app chiusa. La issue #11 +chiede una notifica del tipo «oggi è il compleanno di _tizio_». + +**Decisione** + +1. **Una notifica vera, automatica, su tutti e tre i canali** (DD-038): push, centro notifiche in-app ed + email. Nasce nel database come le altre notifiche automatiche (DD-040): un job `pg_cron` scrive in + `notifiche_utente`, il worker `mailer/` manda push ed email. +2. **Quando:** una volta al giorno, **dalle 8:00 (fuso `Europe/Rome`)**. Il job gira ogni ora e agisce solo + dalle 8 in poi; un registro ricorda a chi l'avviso del giorno è già stato generato, quindi un giro + mancato si recupera all'ora dopo e non si genera mai due volte. +3. **Chi compie gli anni** è chi ha `nascita` con lo stesso giorno e mese di oggi tra i membri **attivi** + della squadra (`giocatori_squadra.attivo`), allenatori compresi. Senza data di nascita o non attivo: + nessun avviso. +4. **Destinatari: tutti i membri attivi**, allenatori compresi (il compleanno è un fatto della squadra, + non una notifica di gioco: DD-039 non si applica). Due messaggi diversi, entrambi **senza l'età**: + - **il festeggiato** riceve **solo gli auguri**: titolo `Buon compleanno, ! 🎂`, **senza corpo**; + - **tutti gli altri** ricevono **solo l'avviso**: titolo `Oggi è il compleanno di `, **senza corpo**. +5. **Più compleanni lo stesso giorno: un solo messaggio per destinatario.** Chi non compie gli anni riceve + un unico avviso con tutti i nomi: titolo `Oggi è il compleanno di e ` (con tre o più: `A, B e C`, + in ordine alfabetico per nome), sempre senza corpo. Ogni festeggiato riceve **solo i propri auguri**, + senza i nomi degli altri. +6. **Registro.** Nuova tabella `compleanni_notificati (giocatore_id, giorno)`: una riga per destinatario e + giorno. Il registro, e non `notifiche_utente`, impedisce il doppione anche se il giocatore elimina la + notifica (stessa logica di DD-037). Il registro non è legato a un evento: i compleanni non sono + `eventi_app`. +7. **Tipi** (`CHECK` di `notifiche_utente`): `compleanno` (l'avviso agli altri) e `compleanno_auguri` (gli + auguri). Nessun pulsante manuale: la notifica parte solo dal job. Il trigger delle push accoda + entrambi; quello delle email li lascia passare, quindi arrivano anche per email. +8. **29 febbraio: per ora non gestito.** Chi è nato il 29 febbraio viene avvisato solo negli anni bisestili, + perché il confronto è tra giorno e mese. Negli altri anni non parte nulla (né il 28 febbraio né il 1° + marzo). È un limite noto, accettato: oggi non interessa nessuno in squadra. + +**Alternative scartate** + +- **Card in Home, senza push né migration** → più semplice, ma non avvisa chi non apre l'app: non è ciò che + chiede la issue. +- **Un messaggio per festeggiato anche a chi lo riceve da altri** → con due compleanni lo stesso giorno + sarebbero due notifiche e due mail al giorno per lo stesso destinatario. +- **Registro per `(destinatario, festeggiato, giorno)`** → serve solo a chi vuole due avvisi separati, che + abbiamo scartato; la chiave `(destinatario, giorno)` basta. +- **Allenatori esclusi** → scartata su indicazione esplicita: «tutti». +- **Gestire il 29 febbraio il 28 febbraio** → rimandato (punto 8). + +**Conseguenze** + +- Ogni giorno con un compleanno ogni membro attivo riceve una notifica e, se ha account, email e le email + accese, una mail. Con molti compleanni nello stesso periodo il tetto giornaliero di invio del worker + (`mailer/`) può farsi sentire: il compleanno non ha priorità sulle altre mail. +- Chi inserisce la propria data di nascita **il giorno stesso** del compleanno, dopo le 8 e dopo che gli + altri sono già stati avvisati, non genera un secondo avviso (il registro ha già il giorno per i + destinatari). Chi la inserisce prima delle 8 viene incluso. +- Un nuovo membro che si aggiunge dopo l'avviso della giornata non riceve quello del giorno. +- Il compleanno resta anche in Calendario, invariato. + +**Riesame** +Se le mail di compleanno risultano troppe, spegnerle e tenere solo push e centro notifiche (come il turno +palloni, DD-040). Se in squadra arriva qualcuno nato il 29 febbraio, gestire quel caso (punto 8). diff --git a/docs/decisions/DD-048.md b/docs/decisions/DD-048.md new file mode 100644 index 0000000..1d3948c --- /dev/null +++ b/docs/decisions/DD-048.md @@ -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. diff --git a/docs/decisions/DD-049.md b/docs/decisions/DD-049.md new file mode 100644 index 0000000..3401e11 --- /dev/null +++ b/docs/decisions/DD-049.md @@ -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. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 67051e9..9166d4b 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -86,6 +86,9 @@ 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-039](DD-039.md) | Niente promemoria all'allenatore | | [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-046](DD-046.md) | Avviso certificati mancanti | | [DD-045](DD-045.md) | Sollecito presenze: un solo avviso a 24 ore | | [DD-044](DD-044.md) | Via la foto tessera dal profilo | diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index 10fb41b..2ceeae3 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -97,7 +97,7 @@ nessuna schermata: | Dove | Cosa si nasconde all'allenatore | | ----------------------------------- | -------------------------------------------------------------------------------- | | `/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`) | | push «Sondaggio pre-partita aperto» | non parte verso i dispositivi degli allenatori (`apri-sondaggio.ts`) | | `/profilo`, `/squadra` | le tab Badge (già escluse sopra) | @@ -132,6 +132,7 @@ Cosa riceve l'allenatore, notifica per notifica (il catalogo completo, con i can | Sollecito presenze | no | L'allenatore non risponde alle presenze (DD-034). Può invece **far partire** il sollecito . | | Turno palloni | no | I turni riguardano solo i giocatori. | | Sondaggio pre-partita | no | Né la push né l'avviso in-app e per email (DD-038): il sondaggio non lo riguarda. | +| Compleanno (DD-047) | **sì** | Riceve l'avviso «Oggi è il compleanno di …» come tutta la squadra, e gli auguri nel giorno del proprio compleanno (la nascita è pubblica, DD-031). In-app, email e push come per i giocatori. | | Notifiche smart (badge, serie, obiettivi, social) | **no** | Le mostra `CelebrazioneBadge` per il giocatore selezionato, letto da `useIo()`, che cerca l'id in `useRosa()`, da cui l'allenatore è escluso: per lui `useIo()` è `null` e `useNotificheSmart()` non fa nulla. Non ha statistiche da cui calcolarle. | Le email arrivano solo se il suo slot ha un account collegato e un'email registrata e se non ha diff --git a/docs/modules/notifiche-email.md b/docs/modules/notifiche-email.md index a2b2662..6c1937e 100644 --- a/docs/modules/notifiche-email.md +++ b/docs/modules/notifiche-email.md @@ -49,7 +49,7 @@ App su Vercel ──scrive──▶ notifiche_utente ──trigger──▶ noti ## Quali notifiche e a chi -**Ogni riga nuova di `notifiche_utente` genera una mail**: messaggio admin, sollecito presenze, sondaggio pre-partita. **Fa eccezione il turno palloni** (DD-040, dalla +**Ogni riga nuova di `notifiche_utente` genera una mail**: messaggio admin, sollecito presenze, sondaggio pre-partita, compleanno (DD-047: solo il titolo, senza corpo; la mail ha oggetto, link e avviso di disattivazione). **Fa eccezione il turno palloni** (DD-040, dalla `m26`): per i tipi `turno_palloni`, `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato` il trigger non accoda nulla, restano push e centro notifiche. Un tipo nuovo che in futuro finisse in quella tabella viaggerebbe per email senza altro lavoro. Le notifiche già presenti quando si applica la migration diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md index 9f3bbed..3da94a6 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -15,7 +15,7 @@ Tenere aggiornati i giocatori senza che debbano aprire l'app, con due meccanismi indipendenti: - **Push VAPID** — arrivano anche ad app chiusa (messaggi dello staff, turno palloni, sollecito - presenze, sondaggio pre-partita: elenco completo nel [catalogo](#catalogo-delle-notifiche)). + presenze, compleanni, sondaggio pre-partita: elenco completo nel [catalogo](#catalogo-delle-notifiche)). - **Notifiche smart** — notifiche locali mostrate solo ad app aperta, generate da badge, serie e obiettivi appena raggiunti; non è un canale push separato. @@ -34,19 +34,20 @@ in-app** (storico in `notifiche_utente`, per tutti i giocatori) ed **email** ([notifiche-email.md](notifiche-email.md): ogni riga di `notifiche_utente` genera una mail, **tranne quelle del turno palloni**, che restano solo push e in-app: DD-040). -| # | Notifica | Cosa la fa partire | Destinatari | Push | In-app | Email | -| --- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------ | --------------- | -| 1 | ~~Promemoria a 24 ore~~ — **rimosso** | Non parte più: i job `pg_cron` sono stati tolti dalla migration `m28` (DD-043). Restano le notifiche già generate (tipo `evento_promemoria_24h`). | — | — | — | — | -| 2 | ~~Promemoria a 3 ore~~ — **rimosso** | Come la riga 1 (tipo `evento_promemoria_3h`). | — | — | — | — | -| 3 | **Messaggio dello staff** — titolo fisso «Messaggio dallo staff», corpo il testo scritto (max 300 caratteri) | Un admin, dalla tab «Notifiche» della dashboard: «Invia messaggio a tutti» o il pulsante della riga di un giocatore. Route `notifica-personalizzata`, tipo `admin`. | Push: tutti i dispositivi iscritti, o quelli del giocatore scelto. In-app ed email: tutta la rosa attiva (allenatori compresi, anche senza dispositivi iscritti), o il giocatore scelto. | sì | sì | sì | -| 4 | **Turno palloni** — «Turno palloni: incarico assegnato» / «Turno palloni: riconsegna» | **Automatica**, **una sola volta, 3 ore prima** dell'evento (DD-040, DD-042): job `pg_cron` ogni 15 minuti, solo per allenamenti e partite con un incaricato confermato; tipo `turno_palloni_3h` (`_12h` e `_6h` esistono solo nelle notifiche generate prima di `m27`), push dal worker. Se l'incaricato cambia, chi aveva già ricevuto un avviso riceve una volta «Turno palloni: incarico revocato» (tipo `turno_palloni_revocato`). **Manuale**, in più: un admin, con il pulsante nella pagina dell'evento; route `promemoria-palloni`, tipo `turno_palloni`, push dalla route. | L'incaricato del turno di quell'evento e, se è un'altra persona, chi aveva i palloni all'evento precedente. Se per l'evento non c'è un incaricato non parte nulla. | sì | sì | **no** (DD-040) | -| 5 | **Sollecito presenze** — «Conferma di partecipazione richiesta: _titolo_» | **Automatica**, **una sola volta, 24 ore prima** dell'evento (DD-040, DD-045): per gli eventi che iniziano entro 24 ore, dallo stesso job; tipo `sollecita_presenze_24h` (`_12h` e `_6h` esistono solo nelle notifiche generate prima di `m30`), push dal worker. **Manuale**, in più: un admin o un allenatore con il pulsante nella pagina dell'evento; route `sollecita-presenze`, tipo `sollecita_presenze`, push dalla route. | Automatica e manuale: i destinatari dell'evento (convocati, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori) che in quel momento non hanno risposto o hanno risposto «forse». | sì | sì | sì | -| 6 | **Sondaggio pre-partita** — «💩 Sondaggio pre-partita aperto» | Un admin, dalla pagina della partita. Route `apri-sondaggio` (vedi [Scout Live](scout-live.md)). | Push: tutti i dispositivi iscritti, tranne quelli degli allenatori. In-app ed email (tipo `sondaggio_cacche`): i giocatori attivi, allenatori esclusi, anche senza dispositivi iscritti. | sì | sì | sì | -| 7 | **Notifiche smart** — badge appena sbloccato, serie a un traguardo, obiettivo di squadra tra il 90 e il 100%, badge social vinto | L'app, mentre è aperta: sono calcolate in locale da `calcolaNotifiche()`. | Chi ha l'app aperta con uno slot giocatore in rosa selezionato; **mai l'allenatore**. Nessun invio verso altri dispositivi. | no (solo locale) | no | no | +| # | Notifica | Cosa la fa partire | Destinatari | Push | In-app | Email | +| --- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------ | --------------- | +| 1 | ~~Promemoria a 24 ore~~ — **rimosso** | Non parte più: i job `pg_cron` sono stati tolti dalla migration `m28` (DD-043). Restano le notifiche già generate (tipo `evento_promemoria_24h`). | — | — | — | — | +| 2 | ~~Promemoria a 3 ore~~ — **rimosso** | Come la riga 1 (tipo `evento_promemoria_3h`). | — | — | — | — | +| 3 | **Messaggio dello staff** — titolo fisso «Messaggio dallo staff», corpo il testo scritto (max 300 caratteri) | Un admin, dalla tab «Notifiche» della dashboard: «Invia messaggio a tutti» o il pulsante della riga di un giocatore. Route `notifica-personalizzata`, tipo `admin`. | Push: tutti i dispositivi iscritti, o quelli del giocatore scelto. In-app ed email: tutta la rosa attiva (allenatori compresi, anche senza dispositivi iscritti), o il giocatore scelto. | sì | sì | sì | +| 4 | **Turno palloni** — «Turno palloni: incarico assegnato» / «Turno palloni: riconsegna» | **Automatica**, **una sola volta, 3 ore prima** dell'evento (DD-040, DD-042): job `pg_cron` ogni 15 minuti, solo per allenamenti e partite con un incaricato confermato; tipo `turno_palloni_3h` (`_12h` e `_6h` esistono solo nelle notifiche generate prima di `m27`), push dal worker. Se l'incaricato cambia, chi aveva già ricevuto un avviso riceve una volta «Turno palloni: incarico revocato» (tipo `turno_palloni_revocato`). **Manuale**, in più: un admin, con il pulsante nella pagina dell'evento; route `promemoria-palloni`, tipo `turno_palloni`, push dalla route. | L'incaricato del turno di quell'evento e, se è un'altra persona, chi aveva i palloni all'evento precedente. Se per l'evento non c'è un incaricato non parte nulla. | sì | sì | **no** (DD-040) | +| 5 | **Sollecito presenze** — «Conferma di partecipazione richiesta: _titolo_» | **Automatica**, **una sola volta, 24 ore prima** dell'evento (DD-040, DD-045): per gli eventi che iniziano entro 24 ore, dallo stesso job; tipo `sollecita_presenze_24h` (`_12h` e `_6h` esistono solo nelle notifiche generate prima di `m30`), push dal worker. **Manuale**, in più: un admin o un allenatore con il pulsante nella pagina dell'evento; route `sollecita-presenze`, tipo `sollecita_presenze`, push dalla route. | Automatica e manuale: i destinatari dell'evento (convocati, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori) che in quel momento non hanno risposto o hanno risposto «forse». | sì | sì | sì | +| 6 | **Sondaggio pre-partita** — «💩 Sondaggio pre-partita aperto» | Un admin, dalla pagina della partita. Route `apri-sondaggio` (vedi [Scout Live](scout-live.md)). | Push: tutti i dispositivi iscritti, tranne quelli degli allenatori. In-app ed email (tipo `sondaggio_cacche`): i giocatori attivi, allenatori esclusi, anche senza dispositivi iscritti. | sì | sì | sì | +| 7 | **Notifiche smart** — badge appena sbloccato, serie a un traguardo, obiettivo di squadra tra il 90 e il 100%, badge social vinto | L'app, mentre è aperta: sono calcolate in locale da `calcolaNotifiche()`. | Chi ha l'app aperta con uno slot giocatore in rosa selezionato; **mai l'allenatore**. Nessun invio verso altri dispositivi. | no (solo locale) | no | no | +| 8 | **Compleanno** — auguri al festeggiato, avviso agli altri | **Automatica**, **una volta al giorno, dalle 8:00** (fuso `Europe/Rome`) per chi compie gli anni quel giorno (DD-047, `m31`): job `pg_cron` ogni ora, funzione `genera_avvisi_compleanno()`, registro `compleanni_notificati`; tipi `compleanno_auguri` e `compleanno`, push dal worker. Nessun pulsante manuale. | **Tutti i membri attivi**, allenatori compresi: il festeggiato riceve gli auguri, gli altri l'avviso. Con più compleanni lo stesso giorno, un solo messaggio a testa (agli altri con tutti i nomi, a ogni festeggiato solo i propri auguri). | sì | sì | sì | -Le notifiche generate dal server (righe 1-6) hanno tutti e tre i canali (DD-038), tranne il turno palloni (riga 4), che non manda email (DD-040). La +Le notifiche generate dal server (righe 1-6 e 8) hanno tutti e tre i canali (DD-038), tranne il turno palloni (riga 4), che non manda email (DD-040). La differenza sta in come parte la push: quelle dei pulsanti (3 e 6) la mandano le route dell'app, quelle -automatiche (4 e 5; la 1 e la 2 non esistono più) nascono nel database e la manda il worker `mailer/` (vedi «Push dei +automatiche (4, 5 e 8; la 1 e la 2 non esistono più) nascono nel database e la manda il worker `mailer/` (vedi «Push dei promemoria»). Il sondaggio riscrive la notifica esistente se l'admin preme di nuovo il pulsante per lo stesso evento (vedi «Centro notifiche in-app»). @@ -63,15 +64,17 @@ volta sola. Nell'email il titolo diventa l'oggetto e il corpo segue il formato d [notifiche-email.md](notifiche-email.md#contenuto-delle-mail) (link «Apri CrAPP» e avviso di disattivazione). Tra `<…>` i valori che cambiano a ogni invio; le date sono `GG/MM/AAAA`. -| # | Notifica | Titolo | Corpo | -| --- | --------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1-2 | Promemoria 24 e 3 ore (rimosso, DD-043) | `Promemoria evento di domani: `, oppure `…di oggi: ` | `Data: domani, `
`Ora: `
`Luogo: ` — «domani» se la data dell'evento è quella di domani, «oggi» altrimenti (fuso `Europe/Rome`, vale per entrambi i promemoria) | -| 3 | Messaggio dello staff | `Messaggio dallo staff` | Il testo scritto dall'admin, max 300 caratteri. | -| 4a | Turno palloni, all'incaricato | `Turno palloni: incarico assegnato` | `Evento: `
`Data: , ore `
`Incarico: custodia dei palloni al termine dell'evento`
`Riconsegna: ` — senza evento successivo la riga «Riconsegna» manca | -| 4b | Turno palloni, a chi li aveva | `Turno palloni: riconsegna` | `Evento: `
`Data: , ore `
`Incarico: riconsegna dei palloni in custodia dal turno precedente` | -| 4c | Turno palloni, revoca | `Turno palloni: incarico revocato` | `Evento: `
`Data: , ore `
`Incarico: non più a tuo carico per questo evento` — a chi aveva ricevuto un avviso (4a o 4b) e non è più destinatario; una sola volta | -| 5 | Sollecito presenze | `Conferma di partecipazione richiesta: ` | `Data: `
`Ora: `
`Luogo: `
`Richiesta di: ` (solo nel sollecito manuale, e solo se il nome c'è)
`Risposta attuale: nessuna` (oppure `forse`)
`Azione: indicare presente, assente o in ritardo` — stesso testo a 24, 12 e 6 ore | -| 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | ` · ore . Quante cacche hai fatto? Rispondi prima del fischio d'inizio.` | +| # | Notifica | Titolo | Corpo | +| --- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1-2 | Promemoria 24 e 3 ore (rimosso, DD-043) | `Promemoria evento di domani: `, oppure `…di oggi: ` | `Data: domani, `
`Ora: `
`Luogo: ` — «domani» se la data dell'evento è quella di domani, «oggi» altrimenti (fuso `Europe/Rome`, vale per entrambi i promemoria) | +| 3 | Messaggio dello staff | `Messaggio dallo staff` | Il testo scritto dall'admin, max 300 caratteri. | +| 4a | Turno palloni, all'incaricato | `Turno palloni: incarico assegnato` | `Evento: `
`Data: , ore `
`Incarico: custodia dei palloni al termine dell'evento`
`Riconsegna: ` — senza evento successivo la riga «Riconsegna» manca | +| 4b | Turno palloni, a chi li aveva | `Turno palloni: riconsegna` | `Evento: `
`Data: , ore `
`Incarico: riconsegna dei palloni in custodia dal turno precedente` | +| 4c | Turno palloni, revoca | `Turno palloni: incarico revocato` | `Evento: `
`Data: , ore `
`Incarico: non più a tuo carico per questo evento` — a chi aveva ricevuto un avviso (4a o 4b) e non è più destinatario; una sola volta | +| 5 | Sollecito presenze | `Conferma di partecipazione richiesta: ` | `Data: `
`Ora: `
`Luogo: `
`Richiesta di: ` (solo nel sollecito manuale, e solo se il nome c'è)
`Risposta attuale: nessuna` (oppure `forse`)
`Azione: indicare presente, assente o in ritardo` — stesso testo a 24, 12 e 6 ore | +| 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | ` · ore . Quante cacche hai fatto? Rispondi prima del fischio d'inizio.` | +| 8a | Compleanno, auguri al festeggiato | `Buon compleanno, ! 🎂` | Nessuno: solo il titolo | +| 8b | Compleanno, avviso agli altri | `Oggi è il compleanno di ` — con più festeggiati `
e `, o `, e `, in ordine alfabetico per nome | Nessuno: solo il titolo. L'età non compare né negli auguri né nell'avviso | Le righe 4a-4c e 5 (e le 1-2, finché esistevano) nascono nelle funzioni SQL della migration `m26` (`genera_promemoria_eventi`, `genera_avvisi_palloni_fascia`, `genera_revoche_palloni`, @@ -103,6 +106,7 @@ dall'emoji. Non entrano nel centro notifiche e non generano email. - Assenze e infortuni. - La registrazione o il collegamento di un nuovo giocatore. - Il certificato in scadenza: resta una card in Home (DD-035). +- Un compleanno non ancora arrivato: l'avviso parte solo il giorno stesso (DD-047). ### Comportamenti da conoscere @@ -143,6 +147,12 @@ descrivono come funzionavano fino alla `m28`: la funzione esiste ancora ma nessu - **Per ogni evento un giocatore riceve al massimo una mail automatica** (DD-045): il sollecito presenze (il promemoria è stato tolto, DD-043), più quelle dei pulsanti manuali (sollecito, sondaggio). Il turno palloni (avvisi automatici, revoca e pulsante) non manda email: restano la push e l'alert in app. +- **Il compleanno parte una volta al giorno, dalle 8:00 (fuso `Europe/Rome`)** (DD-047): il job gira ogni ora e + agisce solo dalle 8; il registro `compleanni_notificati` (per destinatario e giorno) impedisce il doppione anche se + il giocatore elimina la notifica. Chi non ha la data di nascita o non è attivo non è né festeggiato né destinatario. + Un nuovo membro, o chi inserisce la nascita, dopo che gli avvisi del giorno sono partiti non genera un secondo giro. +- **29 febbraio: per ora non gestito** (DD-047). Chi è nato il 29 febbraio è avvisato solo negli anni bisestili: negli + altri non parte nulla, né il 28 febbraio né il 1° marzo. Limite noto e accettato. - **Non esistono preferenze per tipo di notifica:** un solo interruttore per la push («Notifiche») e uno per le email («Email»). Il centro notifiche in-app non si può spegnere. @@ -156,6 +166,7 @@ descrivono come funzionavano fino alla `m28`: la funzione esiste ancora ma nessu | Sollecito presenze: chi non ha risposto o ha detto «forse», mai l'allenatore; permesso ad allenatore e admin, non ai giocatori | `destinatari-notifiche.test.ts`, `unit/presenze.test.ts` | | Sondaggio pre-partita: push senza allenatori, avviso in-app ed email ai giocatori attivi, `upsert`, riservato agli admin | `destinatari-notifiche.test.ts`, `sondaggio-notifiche.test.ts`, `unit/cacche.test.ts` | | Promemoria con «oggi/domani», turno palloni a 3 ore e revoca, solleciti a 24 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`; palloni a 3 ore: DD-042, solleciti a 24 ore: DD-045, `m30`; `m27`) | `integration/notifiche-automatiche.test.ts`, `promemoria-eventi.test.ts` (testo oggi/domani), `destinatari-notifiche.test.ts` (pulsanti), `unit/palloni-core.test.ts`, `unit/presenze.test.ts` | +| Compleanno: ora (anche con l'ora legale), festeggiato e altri, più compleanni lo stesso giorno, una sola volta anche se eliminata, push ed email in coda, 29 febbraio, job pianificato (DD-047, `m31`) | `integration/notifiche-compleanno.test.ts` | | Worker: esiti, backoff, tetto giornaliero, iscrizioni scadute | `unit/mailer-core.test.ts` | | L'allenatore è fuori dalla rosa di gioco | `unit/giocatori-squadra.test.ts` (`inRosa`) | diff --git a/docs/modules/obiettivi-squadra.md b/docs/modules/obiettivi-squadra.md index 248f3bd..3c4c0a5 100644 --- a/docs/modules/obiettivi-squadra.md +++ b/docs/modules/obiettivi-squadra.md @@ -2,7 +2,8 @@ **Stato:** implementato — mesi/scadenze dinamici, target stagionali fissi da rivedere a mano, copertura test completa (unit + integration) su tutti e 10 gli obiettivi. -**File principali:** `src/lib/obiettivi.ts`, `src/lib/rosa.ts` (`useObiettivi()`) +**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 -| id | Obiettivo | Calcolo | Target | Fonte | -| ----- | ---------------------------------- | ----------------------------------------------------------------------------- | --------------------- | ----------------------------- | -| `o1` | 90% presenze del mese | risposte presente/ritardo su partite+allenamenti del mese corrente (dinamico) | 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()` | -| `o12` | Media pagelle da 7.5 | media di tutti i voti, arrotondata a una cifra decimale | 7.5 | `pagelle_voti` | -| `o13` | 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` | -| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` | -| `o3` | Prima vittoria del campionato | `min(vittorie, 1)` | 1 | JSON partite CSI (vedi sotto) | -| `o4` | 5 vittorie in campionato | `min(vittorie, 5)` | 5 | JSON partite CSI | -| `o5` | 10 vittorie in campionato | `min(vittorie, 10)` | 10 | JSON partite CSI | -| `o6` | 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente (dinamico) | 1 | `eventi_app` | +| id | Obiettivo | Calcolo | Target | Fonte | +| ----- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------- | +| `o1` | 90% presenze del mese | presente/ritardo su partite+allenamenti del mese corrente; posti = convocati dell'evento, tutta la rosa se vuoto (dinamico) | 90% | `risposte_presenze` | +| `o2` | Tutti rispondono alle convocazioni | risposte totali / eventi possibili (esclusi i compleanni) | 90% | `risposte_presenze` | +| `o7` | Presenze collettive | % di presenze (presente/ritardo) su tutti gli allenamenti e le partite definiti, passati e futuri; posti = convocati dell'evento (tutta la rosa se vuoto) | 90 | `eventi_app` + `risposte_presenze` | +| `o12` | Media pagelle da 7.5 | media di tutti i voti, arrotondata a una cifra decimale | 7.5 | `pagelle_voti` | +| `o13` | 200 pagelle compilate | conteggio voti | 200 | `pagelle_voti` | +| `o11` | Continuità di squadra | giocatori con ≥3 allenamenti consecutivi | 12 (min. per un 6vs6) | `serieAllenamenti` | +| `o3` | Prima vittoria del campionato | `min(vittorie, 1)` | 1 | JSON partite CSI (vedi sotto) | +| `o4` | 5 vittorie in campionato | `min(vittorie, 5)` | 5 | JSON partite CSI | +| `o5` | 10 vittorie in campionato | `min(vittorie, 10)` | 10 | JSON partite CSI | +| `o6` | 1 evento di squadra al mese | eventi di tipo "evento" nel mese corrente la cui data e ora sono già passate (dinamico) | 1 | `eventi_app` | Mostrati in `squadra.tsx` (elenco completo con barra di progresso) e in `index.tsx` (home: il -primo obiettivo non completato). Un obiettivo che supera il 90% genera anche una notifica -smart (`notifiche-smart.ts`). I target fissi (250 presenze, 200 pagelle, 7.5 di media, 1/5/10 +primo obiettivo non completato). +Ogni obiettivo è **cliccabile**: apre la card di dettaglio (`ObiettivoDrawer`, stesso drawer dei +badge) con stato attuale, come si calcola, cosa conta e cosa no, quando vale, da dove arrivano i +dati e un esempio con numeri. Per le due percentuali di presenze (`o1`, `o7`) la card distingue «Presenza persa» (assente, forse, infortunato, nessuna risposta: il posto resta vuoto) da «Non entra nel calcolo» (cene, eventi di squadra, compleanni). I testi stanno nel campo `dettaglio` di ogni obiettivo in +`obiettivi.ts`: chi cambia il calcolo o il target di un obiettivo aggiorna anche il suo +`dettaglio`, altrimenti la card mente. Un obiettivo che supera il 90% genera anche una notifica +smart (`notifiche-smart.ts`). I target fissi (200 pagelle, 7.5 di media, 1/5/10 vittorie) sono scelte editoriali da rivedere a mano a ogni stagione — nessuna configurazione o UI per farlo, si cambia il numero in `obiettivi.ts`. Fa eccezione "Continuità di squadra" (vedi sotto): il suo target ha un significato specifico, non va scalato come gli altri. @@ -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 (fuso Europe/Rome, `meseCorrente(oggi)`), non più una costante fissa. Per `o1`, titolo ("90% di presenze ad agosto" / "a settembre" / ...) e scadenza (ultimo giorno del mese) -seguono di conseguenza. +seguono di conseguenza. I posti di ogni partita e allenamento sono i convocati (tutta la rosa se non ce +ne sono): chi non è convocato non pesa, mentre assente, forse, infortunato e nessuna risposta sono +presenze perse. + +`o6` si completa quando **arriva l'ora dell'evento**, non quando lo si definisce: conta solo +un evento di tipo "evento" del mese corrente il cui `data` + `ora` (fuso Europe/Rome, +`adessoRoma(oggi)`) è già passato; senza ora vale `00:00`. Un evento futuro lascia l'obiettivo a +0/1 finché non scatta l'ora. Il valore si ricalcola al render: con l'app già aperta il +passaggio a completato compare al refresh successivo. + +`o7` ("Presenze collettive") invece **non** si azzera: copre tutta la stagione, cioè tutti gli +allenamenti e le partite in calendario, passati e futuri (le risposte sugli eventi futuri sono +conferme). Differisce da `o1` solo per il periodo (stagione, non mese): anche in `o1` i posti sono i +convocati di ciascun evento, così chi non è convocato non pesa; con la stessa funzione +`percentualePresenze()`. Non c'è un confine di stagione esplicito: conta ciò +che c'è in `eventi_app`. `o2` ("Tutti rispondono alle convocazioni") non si azzera — aggrega su tutti gli eventi in programma, non solo quelli del mese corrente — ma la sua `scadenza` mostrata in interfaccia è @@ -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 da un backend reale, non solo funzione pura con contesto costruito a mano). -| Obiettivi | Unit test | Integration test | -| ---------- | ----------------------------- | -------------------------------------------------------- | -| o1, o2, o6 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) | -| o7 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) | -| o11 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) | -| o12, o13 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) | -| o3, o4, o5 | `test/unit/obiettivi.test.ts` | `test/integration/api.test.ts` (CSI reale in produzione) | +| Obiettivi | Unit test | Integration test | +| ---------- | ----------------------------- | -------------------------------------------------------------------------------- | +| o1, o2, o6 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale, o1 anche con i convocati) | +| o7 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) | +| o11 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) | +| o12, o13 | `test/unit/obiettivi.test.ts` | `test/integration/obiettivi.test.ts` (Supabase locale) | +| o3, o4, o5 | `test/unit/obiettivi.test.ts` | `test/integration/api.test.ts` (CSI reale in produzione) | +- **Presenze (o1, o7), casi limite** (`test/unit/obiettivi.test.ts`): solo partite e allenamenti + contano, assente/forse/infortunato/nessuna risposta sono posti vuoti, il non convocato non pesa, + confine di mese nel fuso Europe/Rome, convocato fuori rosa, rosa vuota, arrotondamento e soglia + del 90%. Verificato anche con mutazioni (posti = tutta la rosa; eventi di squadra contati): il test + diventa rosso. +- **Card di dettaglio** (`test/unit/obiettivi-dettaglio.test.ts`): ogni obiettivo ha tutti i testi, + «Presenza persa» solo su o1/o7, la card renderizzata mostra tutte le sezioni e lo stato + in corso/completato. - **o1/o2/o6** (Supabase locale): scrive eventi e risposte veri su `eventi_app`/ `risposte_presenze`, li rilegge con `leggiEventi()` (la stessa funzione server dell'app) e una query REST equivalente a `fetchPresenze()`. Copre: contesto vuoto, aggregazione su più eventi, filtro sui tipi (partite/allenamenti contano, eventi sociali/compleanni no), il mese dinamico (evento dentro/fuori mese), scadenza dinamica. -- **o7** (Supabase locale): scrive eventi/presenze reali, calcola `contaPresenzeGiocatore()` (la - stessa funzione pura usata da `useRosa()` in produzione) sui dati riletti, verifica la somma. +- **o7** (Supabase locale): scrive un allenamento e una partita con convocati e presenze reali, + li rilegge con `leggiEventi()`/`leggiPresenze()` e verifica la percentuale (il ritardo conta). - **o11** (Supabase locale): scrive tre allenamenti e presenze reali, calcola `serieConsecutiva()` sui dati riletti, verifica che solo chi resta in serie venga contato. - **o12/o13** (Supabase locale): scrive voti veri su `pagelle_voti` rispettando i vincoli reali diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index d6299ec..b9256c2 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -328,7 +328,6 @@ Per ogni giocatore, nella tab Profili, vengono mostrati. - Stato del profilo - Certificato medico - Documento di identità -- Stato tesseramento CSI (tesserato / da tesserare) Azioni disponibili. @@ -336,7 +335,6 @@ Azioni disponibili. - Scarica certificato - Scarica documento - 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 - Aggiungi giocatore, per inserire un nuovo membro della squadra - Disattiva/Riattiva giocatore, per chi lascia la squadra (o rientra) @@ -362,12 +360,9 @@ Campi esportati. ## Tracciamento tesseramento -Numero e data della tessera CSI non sono dati che il giocatore conosce in anticipo: arrivano -dal comitato dopo l'iscrizione effettiva. Per questo, a differenza dei dati personali del -profilo, li scrive solo un amministratore — come nome, cognome, numero di maglia e ruolo -(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. +Tolto dalla dashboard (DD-048): numero e data della tessera CSI non si registrano più dall'app e non +ci sono più il badge «Tesserato»/«Da tesserare» né il conteggio dei tesserati. Le colonne +`numero_tessera` e `data_tessera` esistono ancora a database, senza uso. ## Completamento profilo diff --git a/docs/modules/squadra.md b/docs/modules/squadra.md index 481f87f..ffbb90f 100644 --- a/docs/modules/squadra.md +++ b/docs/modules/squadra.md @@ -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 «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 -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). ## Gestione dati squadra (solo amministratore) Da `/admin` un amministratore può ([DD-017](../decisions/DD-017.md)): -| Azione | Hook | Effetto | -| ------------------------------ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| 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` (`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 | -| 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`) | +| Azione | Hook | Effetto | +| ----------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| 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` (`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 | +| 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 | Il collegamento giocatore↔account, invece, non è manuale: avviene in automatico al primo accesso con Google, per corrispondenza email diff --git a/package-lock.json b/package-lock.json index 3b96583..38d2fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "crapp", - "version": "1.2.3", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "crapp", - "version": "1.2.3", + "version": "1.3.0", "dependencies": { "@supabase/supabase-js": "^2.111.0", "@tailwindcss/vite": "^4.2.1", diff --git a/package.json b/package.json index 2797ad9..66649a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crapp", - "version": "1.2.3", + "version": "1.3.0", "private": true, "sideEffects": false, "type": "module", diff --git a/src/components/crapp/Avviso.tsx b/src/components/crapp/Avviso.tsx new file mode 100644 index 0000000..48b28cb --- /dev/null +++ b/src/components/crapp/Avviso.tsx @@ -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 = { + 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 ( +
+
+ + + +

+ {titolo} +

+ {etichetta ? ( + + {etichetta} + + ) : null} + {elenco.length > 1 ? ( + + {elenco.length} + + ) : null} + {cliccabile ? : null} +
+ + {testi.map((t) => ( +

+ {t} +

+ ))} + + {elenco.length > 0 ? ( +
    + {elenco.map((r) => ( +
  • + {r.chi} + {r.cosa} +
  • + ))} +
+ ) : null} +
+ ); +} + +/** + * 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 = ; + return ( + + {corpo.cliccabile ? ( + + {card} + + ) : ( +
{card}
+ )} +
+ ); +} diff --git a/src/components/crapp/AvvisoCertificati.tsx b/src/components/crapp/AvvisoCertificati.tsx index e9c1be4..a5c6f83 100644 --- a/src/components/crapp/AvvisoCertificati.tsx +++ b/src/components/crapp/AvvisoCertificati.tsx @@ -1,5 +1,5 @@ -import { AlertCircle } from "lucide-react"; -import { Link } from "@tanstack/react-router"; +import { CalendarClock, FileClock, FileX } from "lucide-react"; +import { Avviso, type RigaAvviso } from "@/components/crapp/Avviso"; import { useProfili } from "@/lib/profili"; import { avvisiCertificatiUtente, @@ -12,45 +12,6 @@ import { oggiISO } from "@/lib/palloni-core"; import { useIsAdmin, useRuoliPronti } from "@/lib/ruoli"; 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 = ( - <> -

- {titolo} -

- {righe.map((r) => ( -

- {r} -

- ))} - - ); - return cliccabile ? ( - - {contenuto} - - ) : ( -
{contenuto}
- ); -} - /** * 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. @@ -72,28 +33,31 @@ export function AvvisoCertificati() { base, }); const cardPersonale = personaleMancante ? ( - ) : personale ? ( personale.giorni < 0 ? ( - ) : ( - ) ) : null; @@ -104,30 +68,33 @@ export function AvvisoCertificati() { <> {cardPersonale} {mancanti.length > 0 ? ( - `${m.nome} ${m.cognome} — non caricato`)} + elenco={mancanti.map((m) => ({ chi: `${m.nome} ${m.cognome}`, cosa: "non caricato" }))} /> ) : null} {scaduti.length > 0 ? ( - rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} + elenco={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} /> ) : null} {inScadenza.length > 0 ? ( - rigaStaff(a, testoScadenza(a.giorni)))} + elenco={inScadenza.map((a) => rigaStaff(a, testoScadenza(a.giorni)))} /> ) : null} ); } -function rigaStaff(a: AvvisoCertificato, dettaglio: string): string { - return `${a.nome} ${a.cognome} — ${dettaglio}`; +function rigaStaff(a: AvvisoCertificato, dettaglio: string): RigaAvviso { + return { chi: `${a.nome} ${a.cognome}`, cosa: dettaglio }; } diff --git a/src/components/crapp/BadgeDrawer.tsx b/src/components/crapp/BadgeDrawer.tsx index f923156..41d2cfe 100644 --- a/src/components/crapp/BadgeDrawer.tsx +++ b/src/components/crapp/BadgeDrawer.tsx @@ -33,7 +33,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
{def.nome} - + {def.descrizione}
@@ -42,13 +42,15 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) { {stato ? (
-

+

Stato attuale

{grado ? ( - {meta?.label} + + {meta?.label} + ) : ( - + In progresso )} @@ -59,19 +61,19 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) { {stato.prossimaSoglia ? ( <> -

+

Mancano {stato.prossimaSoglia - stato.valore} {def.unita} per il{" "} {gradoMeta[stato.prossimo as Grado].label.toLowerCase()}

) : ( -

Hai raggiunto il livello massimo.

+

Hai raggiunto il livello massimo.

)}
) : null}
-

+

Soglie

@@ -88,7 +90,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) { >

@@ -102,7 +104,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) { > {def.soglie[g]}

-

{def.unita}

+

{def.unita}

); })} @@ -110,7 +112,7 @@ function DettaglioBadge({ def, stato }: { def: BadgeDef; stato?: BadgeStato }) {
{def.celebrazione ? ( -

+

“{def.celebrazione}”

) : null} @@ -142,7 +144,7 @@ export function BadgeDrawer({ diff --git a/src/components/crapp/CelebrazioneBadge.tsx b/src/components/crapp/CelebrazioneBadge.tsx index e6df35d..a7d2497 100644 --- a/src/components/crapp/CelebrazioneBadge.tsx +++ b/src/components/crapp/CelebrazioneBadge.tsx @@ -51,11 +51,11 @@ export function CelebrazioneBadge() { {notifica.emoji}
-

+

{notifica.tono === "segreto" ? "Badge segreto sbloccato" : "Traguardo raggiunto"}

{notifica.titolo}

-

{notifica.testo}

+

{notifica.testo}

diff --git a/src/components/crapp/CollezioneBadge.tsx b/src/components/crapp/CollezioneBadge.tsx index 32bbdf1..eb604ff 100644 --- a/src/components/crapp/CollezioneBadge.tsx +++ b/src/components/crapp/CollezioneBadge.tsx @@ -45,21 +45,21 @@ function CardBadge({ b, opaco, indice = 0 }: { b: BadgeStato; opaco?: boolean; i
{meta ? ( - {meta.label} + {meta.label} ) : null}
-

{b.def.nome}

-

+

{b.def.nome}

+

{b.valore} {b.def.unita}

{b.prossimaSoglia ? ( <> -

{mancanoPer(b)}

-

{microcopyBadge(b)}

+

{mancanoPer(b)}

+

{microcopyBadge(b)}

) : ( -

{microcopyBadge(b)}

+

{microcopyBadge(b)}

)} ); @@ -90,13 +90,15 @@ function SocialDrawer({
{cat.nome} - + {cat.descrizione}
-

Vinto

+

+ Vinto +

{conteggio}{" "} @@ -109,7 +111,7 @@ function SocialDrawer({ @@ -139,7 +141,7 @@ export function CollezioneBadge({

-

+

Collezione badge

@@ -147,7 +149,7 @@ export function CollezioneBadge({ /{c.totali}

-

+

completata

@@ -156,12 +158,12 @@ export function CollezioneBadge({ {vicino ? (
-

+

Prossimo traguardo

-

{vicino.def.nome}

+

{vicino.def.nome}

-

+

{mancanoPer(vicino)} · {microcopyBadge(vicino)}

@@ -169,7 +171,7 @@ export function CollezioneBadge({ {c.sbloccati.length > 0 ? (
-

+

Sbloccati ({c.sbloccati.length})

@@ -184,7 +186,7 @@ export function CollezioneBadge({ {c.inProgresso.length > 0 ? (
-

+

In progresso ({c.inProgresso.length})

@@ -199,7 +201,7 @@ export function CollezioneBadge({ {socialVinti.length > 0 ? (
-

+

Votati dai compagni

@@ -207,8 +209,8 @@ export function CollezioneBadge({

{cat.emoji}

-

{cat.nome}

-

+

{cat.nome}

+

Vinto {social[cat.id]} {social[cat.id] === 1 ? "volta" : "volte"}

@@ -219,7 +221,7 @@ export function CollezioneBadge({ ) : null}
-

+

Badge segreti

@@ -229,8 +231,8 @@ export function CollezioneBadge({
-

{b.def.nome}

-

+

{b.def.nome}

+

{b.def.celebrazione ?? "Badge segreto sbloccato."}

@@ -243,8 +245,10 @@ export function CollezioneBadge({ className="grid place-items-center rounded-2xl bg-secondary/60 p-3 text-center" > -

???

-

Badge segreto da scoprire

+

+ ??? +

+

Badge segreto da scoprire

))}
diff --git a/src/components/crapp/DettaglioCsi.tsx b/src/components/crapp/DettaglioCsi.tsx index f7ba10c..c2d934f 100644 --- a/src/components/crapp/DettaglioCsi.tsx +++ b/src/components/crapp/DettaglioCsi.tsx @@ -49,7 +49,7 @@ export function MetaPartitaCsi({ .join(" · "); if (!voci && !link) return null; return ( -
+
{voci ? {voci} : null} {link ? ( -

+

{squadra.squadra}

{squadra.titolari.map((g, i) => ( -
- +
+ {g.numero} {g.nome} - {g.ruolo ? {g.ruolo} : null} + {g.ruolo ? {g.ruolo} : null}
))}
@@ -89,7 +89,7 @@ function ListaFormazione({ squadra }: { squadra: FormazioneSquadra }) {

{squadra.panchina.map((g, i) => ( -
+
{g.numero} {g.nome}
@@ -98,7 +98,7 @@ function ListaFormazione({ squadra }: { squadra: FormazioneSquadra }) { ) : null} {squadra.staff.length > 0 ? ( -
+
{squadra.staff.map((s, i) => (
{s.nome} · {s.ruolo} @@ -121,7 +121,7 @@ function BarraPrecedenti({ }) { const totale = noi + avversario || 1; return ( -
+
{noi} {label} @@ -139,7 +139,7 @@ const NOME_NOI = "CRAP Volley"; /** Intestazione con i nomi delle due squadre, per non dover ripeterli su ogni barra sotto. */ function TestataSquadre({ avversario }: { avversario: string }) { return ( -
+
{NOME_NOI} {avversario}
@@ -158,7 +158,7 @@ function BarraProbabilita({ const favoritaNoi = noi >= avversarioPct; return (
-

+

Probabilità di vittoria (calcolo CSI):{" "} {favoritaNoi ? NOME_NOI : avversario}{" "} favorita al {Math.max(noi, avversarioPct).toFixed(0)}%. @@ -171,13 +171,13 @@ function BarraProbabilita({

{noi.toFixed(0)}%
{avversarioPct.toFixed(0)}% @@ -197,7 +197,7 @@ function BloccoPrecedenti({ return (
-

+

{precedenti.totale > 0 ? `${precedenti.totale} precedenti in archivio sul portale CSI.` : "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); if (isLoading) { - return

Carico i dati dal CSI…

; + return ( +

Carico i dati dal CSI…

+ ); } if (!data || (!data.formazioni && !data.precedenti)) return null; return (
{data.giornata || data.nota ? ( -

+

{[data.giornata, data.nota].filter(Boolean).join(" · ")}

) : null} {data.formazioni ? ( -
+
Formazioni
@@ -272,7 +274,7 @@ export function DettaglioCsiEsteso({ ) : null} {data.precedenti ? ( -
+
Scontri diretti
diff --git a/src/components/crapp/EventoCard.tsx b/src/components/crapp/EventoCard.tsx index 7b6cb51..36be015 100644 --- a/src/components/crapp/EventoCard.tsx +++ b/src/components/crapp/EventoCard.tsx @@ -101,10 +101,10 @@ export function EventoCard({
-

{evento.titolo}

-

{evento.luogo}

+

{evento.titolo}

+

{evento.luogo}

-
+
{etichettaData(evento.data)}
@@ -139,13 +139,13 @@ export function EventoCard({
{tipo.label} - + {etichettaData(evento.data)} {linkTo ? : null} @@ -153,10 +153,10 @@ export function EventoCard({

{evento.titolo}

{sottotitoloPartita ? ( -

{sottotitoloPartita}

+

{sottotitoloPartita}

) : null} -
+
{evento.ora} @@ -174,12 +174,12 @@ export function EventoCard({
{noteCard ? ( -

📝 {noteCard}

+

📝 {noteCard}

) : null} {metaStato ? (

{metaStato.emoji} diff --git a/src/components/crapp/ObiettivoDrawer.tsx b/src/components/crapp/ObiettivoDrawer.tsx new file mode 100644 index 0000000..90d7803 --- /dev/null +++ b/src/components/crapp/ObiettivoDrawer.tsx @@ -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 ( +

+

+ {titolo} +

+
    + {voci.map((v) => ( +
  • + + {segno} + + {v} +
  • + ))} +
+
+ ); +} + +function Blocco({ titolo, testo }: { titolo: string; testo: string }) { + return ( +
+

+ {titolo} +

+

{testo}

+
+ ); +} + +/** 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 ( +
+
+ + {o.emoji} + +
+ {o.titolo} + + {o.descrizione} + +
+
+ +
+
+

+ Stato attuale +

+ + {fatto ? "Completato" : "In corso"} + +
+

+ {o.valore} su {o.target}{" "} + {o.unita} +

+ +

+ {pct}%{o.scadenza ? ` · entro il ${formatData(o.scadenza)}` : ""} +

+

{microcopyObiettivo(o)}

+
+ + + + {d.perse ? : null} + + + + + +

+ {o.impatto} +

+
+ ); +} + +/** 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 ( + + {children} + + + {obiettivo.titolo} + {obiettivo.descrizione} + + + + + + + + + + ); +} diff --git a/src/components/crapp/Pagelle.tsx b/src/components/crapp/Pagelle.tsx index 484074e..9d24f22 100644 --- a/src/components/crapp/Pagelle.tsx +++ b/src/components/crapp/Pagelle.tsx @@ -45,19 +45,19 @@ export function Pagelle({ return (
-

+

Pagelle anonime

- + {chiuse ? "Votazioni chiuse" : `${fatti}/${daVotare.length} votati`}
-

+

Dai un voto da 1 a 10 ai compagni: nessuno vedrà chi ha votato cosa, solo la media.

{isPending ? ( -

+

Carico le pagelle…

) : ( @@ -70,15 +70,17 @@ export function Pagelle({ return (
- + - {g.nome} - + + {g.nome} + + {media ? `Media ${media.media} · ${media.voti} voti` : "Nessun voto"} {sonoIo ? ( - + Sei tu ) : chiuse ? ( @@ -89,7 +91,7 @@ export function Pagelle({ disabled={!io || vota.isPending} onClick={() => setApertoPer(aperto ? null : g.id)} 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 ? "bg-accent text-accent-foreground" : "bg-card text-foreground", @@ -107,7 +109,7 @@ export function Pagelle({ type="button" onClick={() => invia(g.id, v)} 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 ? "bg-accent text-accent-foreground" : "bg-card text-foreground", diff --git a/src/components/crapp/ProfiloAmministrativo.tsx b/src/components/crapp/ProfiloAmministrativo.tsx index 17cfdee..60bc5e6 100644 --- a/src/components/crapp/ProfiloAmministrativo.tsx +++ b/src/components/crapp/ProfiloAmministrativo.tsx @@ -27,7 +27,7 @@ const TIPI_DOCUMENTO = ["Carta d'identità", "Patente", "Passaporto"]; function Intestazione({ titolo, completa }: { titolo: string; completa: boolean }) { return (
-

{titolo}

+

{titolo}

{completa ? : null}
); @@ -67,7 +67,7 @@ function CampoFile({ return (
- + input.current?.click()} 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 ? : path ? "Sostituisci" : "Carica"} @@ -336,7 +336,7 @@ export function ProfiloAllenatore({ g }: { g: GiocatoreSquadra }) { style={{ width: `${perc}%` }} />
- + {perc}%
@@ -366,7 +366,7 @@ export function ProfiloAllenatore({ g }: { g: GiocatoreSquadra }) { type="button" onClick={() => void salva()} 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 ? : null} {sporco ? "Salva" : "Salvato"} @@ -430,7 +430,7 @@ export function ProfiloAmministrativo({ style={{ width: `${perc}%` }} />
- + {perc}%
@@ -472,7 +472,7 @@ export function ProfiloAmministrativo({ type="button" onClick={salvaBozza} 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 ? : null} {sporco ? "Salva" : "Salvato"} @@ -486,7 +486,9 @@ export function ProfiloAmministrativo({ {perc}%
} + azione={ + {perc}% + } > {corpo} @@ -521,10 +523,10 @@ export function CompletaProfilo({ className="premi block rounded-3xl bg-card p-4 shadow-card" >
- + Completa il tuo profilo - {perc}% + {perc}%
-

+

{allenatore ? "Data e luogo di nascita, telefono ed email." : "Documento e certificato medico servono per il tesseramento CSI."} diff --git a/src/components/crapp/PromemoriaPalloni.tsx b/src/components/crapp/PromemoriaPalloni.tsx index 1de634a..807b923 100644 --- a/src/components/crapp/PromemoriaPalloni.tsx +++ b/src/components/crapp/PromemoriaPalloni.tsx @@ -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 { eventiPalloni, eventoPrecedente, eventoSuccessivo, oggiISO } from "@/lib/palloni-core"; import { useTurniPalloni } from "@/lib/palloni"; @@ -17,6 +18,8 @@ export function PromemoriaPalloni() { const oggi = oggiISO(); const messaggi: string[] = []; + // «Oggi» solo se c'è davvero qualcosa da fare oggi; altrimenti è un semplice promemoria. + let oggiTocca = true; for (const evento of lista) { if (evento.data !== oggi) continue; @@ -36,6 +39,7 @@ export function PromemoriaPalloni() { } if (messaggi.length === 0) { + oggiTocca = false; const prossimo = lista.find((e) => e.data >= oggi && turni[e.id] === io.id); if (!prossimo) return null; messaggi.push( @@ -44,15 +48,13 @@ export function PromemoriaPalloni() { } return ( -

-

- Turno palloni -

- {messaggi.map((m) => ( -

- {m} -

- ))} -
+ ); } diff --git a/src/components/crapp/RosaPresenze.tsx b/src/components/crapp/RosaPresenze.tsx index f02d065..ad9c9cf 100644 --- a/src/components/crapp/RosaPresenze.tsx +++ b/src/components/crapp/RosaPresenze.tsx @@ -70,7 +70,7 @@ export function RosaPresenze({
-

+

Hanno risposto {risposteN}/{rosa.length}

{perc}% @@ -84,7 +84,7 @@ export function RosaPresenze({ 0 ? statoMeta[s].className : "bg-secondary text-muted-foreground", )} > @@ -92,14 +92,14 @@ export function RosaPresenze({ ); })} - + ❔ {mancanti.length} da rispondere
{io ? (
-

+

La tua risposta

@@ -114,7 +114,7 @@ export function RosaPresenze({ salva.mutate({ eventoId, giocatoreId: io.id, stato: attivo ? null : s }) } 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 ? cn(statoMeta[s].className, "border-transparent shadow-card") : "bg-background text-muted-foreground", @@ -133,7 +133,7 @@ export function RosaPresenze({ type="button" onClick={sollecita} 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 ? ( @@ -146,7 +146,7 @@ export function RosaPresenze({ {isPending ? ( -

+

Carico le risposte…

) : null} @@ -194,16 +194,16 @@ function Gruppo({ attenzione && "border border-dashed border-warning", )} > -

+

{titolo} · {n} {attenzione ? : null}

{lista.map((g) => (
- - {g.nome} - {g.ruolo} + + {g.nome} + {g.ruolo}
))}
diff --git a/src/components/crapp/ScoutEntry.tsx b/src/components/crapp/ScoutEntry.tsx index 6caa845..d67cb79 100644 --- a/src/components/crapp/ScoutEntry.tsx +++ b/src/components/crapp/ScoutEntry.tsx @@ -37,7 +37,7 @@ export function ScoutEntry({ {occupato ? : } {titolo} - {sottotitolo} + {sottotitolo} {disponibile ? : null} diff --git a/src/components/crapp/SerieCard.tsx b/src/components/crapp/SerieCard.tsx index 474e841..395ae44 100644 --- a/src/components/crapp/SerieCard.tsx +++ b/src/components/crapp/SerieCard.tsx @@ -28,8 +28,8 @@ export function SerieGriglia({ g }: { g: Giocatore }) {
-

{s.def.label}

-

{s.def.descrizione}

+

{s.def.label}

+

{s.def.descrizione}

-

+

{s.prossimo ? `${s.valore}/${s.prossimo} · ` : ""} {s.messaggio}

@@ -61,17 +61,17 @@ export function SerieHome({ g }: { g: Giocatore }) {
-

+

Serie {top.def.label.toLowerCase()}: {top.valore}

-

{top.messaggio}

+

{top.messaggio}

{serieGiocatore(g).map((s) => (

{s.valore}

-

+

{s.def.label}

diff --git a/src/components/crapp/SondaggioCacche.tsx b/src/components/crapp/SondaggioCacche.tsx index 59a0213..ef5b58a 100644 --- a/src/components/crapp/SondaggioCacche.tsx +++ b/src/components/crapp/SondaggioCacche.tsx @@ -81,10 +81,10 @@ export function SondaggioCacche({ if (!sondaggioAperto(dataEvento, oraEvento)) { return ( -

+

💩 Sondaggio pre-partita

-

+

{sondaggioTerminato(dataEvento, oraEvento) ? "Sondaggio chiuso: era aperto fino al fischio d'inizio." : "Apre alle 8:00 del giorno della partita: riceverai una notifica."} @@ -96,14 +96,14 @@ export function SondaggioCacche({ return (

-

+

💩 Sondaggio pre-partita

- + {dellaPartita.length} risposte
-

+

Quante cacche hai fatto prima di questa partita? Dato scientifico fondamentale.

@@ -115,7 +115,7 @@ export function SondaggioCacche({ disabled={!io || salva.isPending} onClick={() => rispondi(n)} 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 ? "bg-accent text-accent-foreground shadow-pop" : "bg-secondary text-foreground", @@ -126,7 +126,7 @@ export function SondaggioCacche({ ))}
-
+
Media squadra {media} @@ -145,7 +145,7 @@ export function SondaggioCacche({ type="button" onClick={avvisaTutti} 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 ? : } Avvisa tutti del sondaggio diff --git a/src/components/crapp/TurnoPalloni.tsx b/src/components/crapp/TurnoPalloni.tsx index ff4bfb5..59e289d 100644 --- a/src/components/crapp/TurnoPalloni.tsx +++ b/src/components/crapp/TurnoPalloni.tsx @@ -74,16 +74,16 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) { - + Palloni - + {isPending || assegna.isPending ? ( ) : null} {giocatore ? nomeCompleto(giocatore) : "Da assegnare"} {proposto && giocatore ? ( - + proposto ) : null} @@ -100,11 +100,11 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) { type="button" onClick={() => scegli(g.id)} 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", )} > - + {nomeCompleto(g)} {g.id === id ? : null} @@ -117,7 +117,7 @@ export function TurnoPalloni({ eventoId }: { eventoId: string }) { type="button" onClick={avvisa} 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 ? ( diff --git a/src/components/crapp/VotazioneMvp.tsx b/src/components/crapp/VotazioneMvp.tsx index 3de0a90..08adfb7 100644 --- a/src/components/crapp/VotazioneMvp.tsx +++ b/src/components/crapp/VotazioneMvp.tsx @@ -46,10 +46,10 @@ export function VotazioneMvp({ evento }: { evento: Evento }) { if (!votoMvpAperto(evento.data, evento.ora)) { return (
-

+

Voto MVP

-

+

Apre {ORE_ATTESA_MVP} ore dopo l'inizio della partita.

@@ -75,20 +75,20 @@ export function VotazioneMvp({ evento }: { evento: Evento }) { return (
-

+

Voto MVP · {totale} {totale === 1 ? "voto" : "voti"}

-

+

{testa && !pareggio ? ( {testa.nome} ({testa.voti}) @@ -100,9 +100,9 @@ export function VotazioneMvp({ evento }: { evento: Evento }) { )}

{mio ? ( -

Hai votato {mio.votato_nome}

+

Hai votato {mio.votato_nome}

) : !puoVotare ? ( -

+

Vota chi era presente a questa partita.

) : null} @@ -120,7 +120,7 @@ export function VotazioneMvp({ evento }: { evento: Evento }) { disabled={vota.isPending} onClick={() => invia(g.id, nomeCompleto(g))} 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 ? "bg-accent text-accent-foreground" : "bg-card text-foreground", @@ -133,7 +133,7 @@ export function VotazioneMvp({ evento }: { evento: Evento }) { ) : conteggio.length > 0 ? (
{conteggio.map((c) => ( - + {c.nome} · {c.voti} ))} diff --git a/src/components/crapp/VotoSocial.tsx b/src/components/crapp/VotoSocial.tsx index 60468ab..c38f63d 100644 --- a/src/components/crapp/VotoSocial.tsx +++ b/src/components/crapp/VotoSocial.tsx @@ -51,7 +51,7 @@ export function VotoSocial({ matchId }: { matchId: string }) { return (
-
+
Hai votato {fatti}/{categorieSocial.length} categorie · un solo voto per categoria @@ -79,14 +79,16 @@ export function VotoSocial({ matchId }: { matchId: string }) { {cat.emoji} - {cat.nome} - + + {cat.nome} + + {mio ? `Hai votato ${mio.votato_nome}` : cat.descrizione} @@ -105,7 +107,7 @@ export function VotoSocial({ matchId }: { matchId: string }) { disabled={vota.isPending} onClick={() => invia(cat.id, g.id, nomeCompleto(g))} 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 ? "bg-accent text-accent-foreground" : "bg-secondary text-foreground", @@ -118,17 +120,17 @@ export function VotoSocial({ matchId }: { matchId: string }) { ) : (
{totale === 0 ? ( -

+

Nessun voto ancora: apri e scegli un compagno.

) : vincitore ? ( -

+

{vincitore.nome} · {vincitore.voti} {vincitore.voti === 1 ? "voto" : "voti"}

) : ( -

+

Parità con {totale} voti: servono altri voti per assegnare il badge.

)} diff --git a/src/components/crapp/ui-bits.tsx b/src/components/crapp/ui-bits.tsx index 9080813..3310eda 100644 --- a/src/components/crapp/ui-bits.tsx +++ b/src/components/crapp/ui-bits.tsx @@ -107,13 +107,13 @@ function RigaNotifica({ const arrivo = info.offset.x + proietta(info.velocity.x); 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]" >

{notifica.titolo}

{notifica.corpo ? (

{notifica.corpo}

) : null} -

{tempoRelativo(notifica.creataIl)}

+

{tempoRelativo(notifica.creataIl)}

{azione ?? } @@ -338,7 +338,7 @@ export function SezioneTendina({ * parità di padding — un'altezza esplicita allinea tutti i campi tra loro. */ 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 @@ -349,7 +349,7 @@ export const classiInput = export function Campo({ label, children }: { label: string; children: ReactNode }) { return (
); } diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index fc12725..b3a1e2c 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -78,7 +78,7 @@ const DrawerDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/src/lib/giocatori-squadra.ts b/src/lib/giocatori-squadra.ts index 7df6258..99d5448 100644 --- a/src/lib/giocatori-squadra.ts +++ b/src/lib/giocatori-squadra.ts @@ -222,40 +222,6 @@ export function useSalvaDatiSquadra() { }); } -/** Numero e data della tessera CSI, note solo dopo il tesseramento effettivo. */ -export type DatiTesseramento = Pick; - -/** - * 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(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. * L'id (`g`) non è generato dal database: va calcolato con `prossimoIdGiocatore` diff --git a/src/lib/notifiche-utente.ts b/src/lib/notifiche-utente.ts index 8310b0f..a02835d 100644 --- a/src/lib/notifiche-utente.ts +++ b/src/lib/notifiche-utente.ts @@ -24,7 +24,9 @@ export type TipoNotifica = | "sollecita_presenze_24h" | "sollecita_presenze_12h" | "sollecita_presenze_6h" - | "sondaggio_cacche"; + | "sondaggio_cacche" + | "compleanno" + | "compleanno_auguri"; export type NotificaUtente = { id: string; diff --git a/src/lib/obiettivi.ts b/src/lib/obiettivi.ts index 2ba8d47..7c9064f 100644 --- a/src/lib/obiettivi.ts +++ b/src/lib/obiettivi.ts @@ -14,6 +14,25 @@ export type ObiettivoSquadra = { emoji: string; /** Frase breve che spiega come ogni giocatore può spostare l'ago. */ 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 = { @@ -31,6 +50,24 @@ function meseCorrente(oggi: Date): string { 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. */ function aMese(oggi: Date): string { 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")}`; } -function percentualePresenzeMese(ctx: ContestoObiettivi, rosaSize: number, mese: string) { - const delMese = ctx.eventi.filter( - (e) => e.data.startsWith(mese) && (e.tipo === "partita" || e.tipo === "allenamento"), - ); - if (delMese.length === 0 || rosaSize === 0) return 0; - const posti = delMese.length * rosaSize; - const presenti = delMese.reduce((s, e) => { +/** + * Presenze (presente o ritardo) sul totale dei posti degli allenamenti e delle partite scelti da + * `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. + */ +function percentualePresenze( + 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] ?? {}; - return s + Object.values(risposte).filter((x) => x === "presente" || x === "ritardo").length; - }, 0); + presenti += attesi.filter((g) => { + const stato = risposte[g.id]; + return stato === "presente" || stato === "ritardo"; + }).length; + } + if (posti === 0) return 0; return Math.round((presenti / posti) * 100); } @@ -79,7 +130,6 @@ export function obiettiviSquadra( ctx: ContestoObiettivi = contestoVuoto, oggi: Date = new Date(), ): 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 vittorie = ctx.vittorie ?? 0; const mese = meseCorrente(oggi); @@ -88,12 +138,35 @@ export function obiettiviSquadra( id: "o1", titolo: `90% di presenze ${aMese(oggi)}`, 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, unita: "%", scadenza: fineMese(oggi), emoji: "📣", 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", @@ -105,16 +178,55 @@ export function obiettiviSquadra( scadenza: fineMese(oggi), emoji: "⚡", 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", - titolo: "250 presenze complessive", - descrizione: "Somma delle presenze di tutta la rosa in stagione", - valore: somma((g) => g.presenze), - target: 250, - unita: "presenze", + titolo: "Presenze collettive", + descrizione: "Presenze di tutta la rosa su allenamenti e partite definiti in stagione", + valore: percentualePresenze(ctx, rosa), + target: 90, + unita: "%", emoji: "🤝", 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", @@ -125,6 +237,19 @@ export function obiettiviSquadra( unita: "di media", emoji: "📝", 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", @@ -135,6 +260,16 @@ export function obiettiviSquadra( unita: "voti", emoji: "🗳️", 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", @@ -145,6 +280,21 @@ export function obiettiviSquadra( unita: "giocatori", emoji: "🔗", 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", @@ -155,6 +305,22 @@ export function obiettiviSquadra( unita: "vittorie", emoji: "🎉", 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", @@ -165,6 +331,22 @@ export function obiettiviSquadra( unita: "vittorie", emoji: "🔥", 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", @@ -175,16 +357,56 @@ export function obiettiviSquadra( unita: "vittorie", emoji: "🏆", 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", titolo: "1 evento di squadra al mese", 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, unita: "eventi", emoji: "🍕", 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.", + }, }, ]; } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 3171510..ea89f5c 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -27,13 +27,13 @@ function NotFoundComponent() {

404

Pagina non trovata

-

+

La pagina che cerchi non esiste o è stata spostata.

Torna alla home @@ -53,7 +53,7 @@ function ErrorComponent({ error, reset }: { error: unknown; reset: () => void })

Questa pagina non si è caricata

-

+

Qualcosa è andato storto da parte nostra. Puoi riprovare o tornare alla home.

@@ -63,13 +63,13 @@ function ErrorComponent({ error, reset }: { error: unknown; reset: () => void }) router.invalidate(); 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 Torna alla home diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index b61ba01..1c9226d 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { - BadgeCheck, Bell, BellOff, ChevronDown, @@ -33,11 +32,9 @@ import { useGiocatoriSquadra, useImpostaAttivo, useSalvaDatiSquadra, - useSalvaTesseramento, useScollegaAccount, validaDatiSquadra, type DatiSquadra, - type DatiTesseramento, type GiocatoreSquadra, } from "@/lib/giocatori-squadra"; import { scaricaFile, useProfili, useSalvaProfilo } from "@/lib/profili"; @@ -110,18 +107,11 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi const { righe } = useGiocatoriSquadra(); const salvaSquadra = useSalvaDatiSquadra(); const salvaProfilo = useSalvaProfilo(); - const salvaTesseramento = useSalvaTesseramento(); const scollega = useScollegaAccount(); const impostaAttivo = useImpostaAttivo(); const [datiSquadra, setDatiSquadra] = useState(null); const [bozza, setBozza] = useState(null); - const [tesseramento, setTesseramento] = useState(null); - - const tesseramentoCorrente: DatiTesseramento = tesseramento ?? { - numeroTessera: g.numeroTessera, - dataTessera: g.dataTessera, - }; const squadraCorrente: DatiSquadra = datiSquadra ?? { nome: g.nome, @@ -131,7 +121,7 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi email: g.email, 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 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() { try { await salvaProfilo.mutateAsync(profiloCorrente); @@ -204,7 +184,7 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi return (
-

Dati squadra

+

Dati squadra

Salva dati squadra - {allenatore ? null : ( - <> -

Tesseramento CSI

-
- - - setTesseramento({ - ...tesseramentoCorrente, - numeroTessera: e.target.value || null, - }) - } - className={classiInput} - /> - - - - setTesseramento({ ...tesseramentoCorrente, dataTessera: e.target.value || null }) - } - className={classiInput} - /> - -
- - - )} - {allenatore ? ( <> -

Dati personali

+

Dati personali

setBozza({ ...profiloCorrente, ...patch })} @@ -328,12 +268,12 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi type="button" onClick={confermaProfilo} 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 -
+
{g.authUserId ? "Account collegato" : "Nessun account collegato"} @@ -404,7 +344,7 @@ function Documento({ onClick={scarica} disabled={!path || inCorso} 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], )} > @@ -442,12 +382,12 @@ function SchedaGiocatore({ return ( @@ -558,7 +489,7 @@ function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) { return (
-

+

{allenatore ? "Nuovo allenatore" : "Nuovo giocatore"}

@@ -632,7 +563,7 @@ function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) { setAperto(false); 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 @@ -640,7 +571,7 @@ function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) { type="button" onClick={() => void conferma()} 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 @@ -667,7 +598,7 @@ function GiocatoreDisattivato({ g }: { g: GiocatoreSquadra }) {

{nomeCompleto(g)}

-

+

{isAllenatore(g) ? ruoloVisibile(g) : `#${g.numero} · ${g.ruolo}`}

@@ -675,7 +606,7 @@ function GiocatoreDisattivato({ g }: { g: GiocatoreSquadra }) { type="button" onClick={() => void riattiva()} 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" > Riattiva @@ -722,7 +653,7 @@ function Dashboard() { <>
-

+

Riservata agli amministratori della squadra.

@@ -743,7 +674,6 @@ function Dashboard() { statoScadenza(profili[g.id]?.certificatoScadenza, profili[g.id]?.certificatoPath, oggi) === "valido", ).length; - const tesserati = attivi.filter((g) => g.numeroTessera).length; const contenutoSquadra = ( <> @@ -755,12 +685,11 @@ function Dashboard() { label="Certificati validi" hint="non scaduti" /> -
@@ -769,7 +698,7 @@ function Dashboard() { ); const contenutoProfili = isPending ? ( -

+

Caricamento…

) : ( @@ -786,7 +715,7 @@ function Dashboard() { /> ))} {allenatori.length > 0 ? ( -

Allenatori

+

Allenatori

) : null} {allenatori.map((g, i) => ( 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" > Invia messaggio a tutti @@ -835,7 +764,7 @@ function Dashboard() { )}

@@ -855,7 +784,7 @@ function Dashboard() {

) : ( -

+

Caricamento…

); @@ -909,7 +838,7 @@ function Dashboard() { type="button" onClick={inviaMessaggio} 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 ? ( @@ -921,7 +850,7 @@ function Dashboard() { diff --git a/src/routes/allenamento.$id.tsx b/src/routes/allenamento.$id.tsx index 72bf46e..2a6e031 100644 --- a/src/routes/allenamento.$id.tsx +++ b/src/routes/allenamento.$id.tsx @@ -43,7 +43,9 @@ function AllenamentoDetail() { if (!evento) { return (
-

Allenamento non trovato

+

+ Allenamento non trovato +

); } @@ -59,14 +61,14 @@ function AllenamentoDetail() {
-

+

Allenamento di squadra

{evento.titolo}

-
+
{evento.ora} @@ -76,12 +78,12 @@ function AllenamentoDetail() {
{evento.note ? ( -

+

{evento.note}

) : null} -
+
Conferme: {presentiVeri}/{convocatiEvento(evento, rosa).length}
diff --git a/src/routes/benvenuto.tsx b/src/routes/benvenuto.tsx index c20cda0..ab3dd7d 100644 --- a/src/routes/benvenuto.tsx +++ b/src/routes/benvenuto.tsx @@ -107,34 +107,34 @@ function Benvenuto() { {!pronta ? null : !utenteId ? ( <> -

+

Accedi con il tuo account Google per collegare il profilo giocatore.

) : erroreCollegamento ? ( <> -

+

Nessun profilo trovato per la tua email. Contatta un amministratore per collegare il tuo account.

) : inAttesaCollegamento ? ( -

+

Ti stiamo collegando al tuo profilo...

) : null} diff --git a/src/routes/calendario.tsx b/src/routes/calendario.tsx index 3a35251..d8e0120 100644 --- a/src/routes/calendario.tsx +++ b/src/routes/calendario.tsx @@ -119,7 +119,7 @@ function Calendario() {
-
+
{giorniIT.map((g, i) => ( {g} ))} @@ -192,7 +192,7 @@ function Calendario() { onClick={haEventi ? () => apriGiorno(giorno) : undefined} style={sfondo ? { backgroundImage: sfondo } : undefined} 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 === "allenamento" && "bg-training text-training-foreground", tipo === "evento" && "bg-warning text-warning-foreground", @@ -224,10 +224,10 @@ function Calendario() {
-

+

Scorri a destra o sinistra per cambiare mese.

-
+
Partita @@ -248,7 +248,7 @@ function Calendario() {
Gestisci eventi @@ -263,7 +263,7 @@ function Calendario() { return ; }) ) : ( -

+

Nessun evento in programma

)} @@ -275,7 +275,7 @@ function Calendario() { {compleanniMese.length > 0 ? ( compleanniMese.map((c) => ) ) : ( -

+

Nessun compleanno in {mesiIT[mese]!.toLowerCase()}

)} @@ -300,7 +300,7 @@ function Calendario() { return ; }) ) : ( -

+

Nessun evento in questa data

)} diff --git a/src/routes/classifica.tsx b/src/routes/classifica.tsx index c0208ba..a06abed 100644 --- a/src/routes/classifica.tsx +++ b/src/routes/classifica.tsx @@ -82,7 +82,7 @@ export const Route = createFileRoute("/classifica")({ function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: string }) { return (
-
+
# Squadra G @@ -90,7 +90,7 @@ function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: s Pt
{righe.length === 0 ? ( -

{vuoto}

+

{vuoto}

) : ( righe.map((r) => { const noi = isNostraSquadra(r.squadra) || r.squadra === "CRAP Volley"; @@ -98,14 +98,14 @@ function TabellaClassifica({ righe, vuoto }: { righe: RigaClassifica[]; vuoto: s
{r.pos} {r.squadra} - {r.giocate} - + {r.giocate} + {r.setFatti}:{r.setSubiti} {r.punti} @@ -171,7 +171,7 @@ function Classifica() { <>
{classificaCoppa.length > 0 ? ( <> -

Coppa

+

Coppa

-

Girone

+

Girone

) : null} @@ -204,14 +204,14 @@ function Classifica() { const contenutoStorico = useMemo( () => tuttiMatch.length === 0 ? ( -

+

Nessuna partita disponibile.

) : (
{raggruppaPerStagione(tuttiMatch).map(({ stagione, match }) => (
-

Stagione {stagione}

+

Stagione {stagione}

{match.map((m) => { const vinta = m.setNostri > m.setLoro; const eventoId = eventoIdPerData.get(m.data); @@ -237,7 +237,7 @@ function Classifica() { logoAvversario={m.logoAvversario} avversario={m.avversario} /> -

{casa.nome}

+

{casa.nome}

-

{trasferta.nome}

+

{trasferta.nome}

-

+

{formatData(m.data)} · MVP {m.mvp || "da votare"} {m.scout ? " · scoutata" : ""}

@@ -278,7 +278,7 @@ function Classifica() { @@ -287,7 +287,7 @@ function Classifica() { ); })} {eventoId && !m.mvp ? ( - + Vota MVP ) : null} diff --git a/src/routes/eventi.tsx b/src/routes/eventi.tsx index 8f485c7..0bfd065 100644 --- a/src/routes/eventi.tsx +++ b/src/routes/eventi.tsx @@ -91,7 +91,7 @@ function GestioneEventi() { <>
-

+

Solo i referenti della squadra possono creare o modificare gli eventi.

@@ -207,7 +207,7 @@ function GestioneEventi() {
-
+
{giorniIT.map((g, i) => ( {g} ))} @@ -248,7 +248,7 @@ function GestioneEventi() { type="button" onClick={() => apriGiorno(giorno)} 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 ? "bg-accent text-accent-foreground" : "bg-secondary text-foreground", @@ -265,11 +265,11 @@ function GestioneEventi() {
{isPending ? ( -

+

Carico gli eventi…

) : isError ? ( -
+

Non sono riuscito a caricare gli eventi {error instanceof Error ? `: ${error.message}` : ""}. @@ -277,13 +277,13 @@ function GestioneEventi() {

) : ( -

+

Scorri a destra o sinistra per cambiare mese. Tocca un giorno per aggiungere, modificare o eliminare.

@@ -303,7 +303,7 @@ function GestioneEventi() { type="button" onClick={() => aggiorna(daCategoria(t.id))} 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 ? "bg-card shadow-card text-foreground" : "text-muted-foreground", @@ -337,15 +337,17 @@ function GestioneEventi() { ) : (
- + Giorno
- {formatData(bozza.data)} + + {formatData(bozza.data)} + @@ -394,7 +396,7 @@ function GestioneEventi() { type="button" onClick={() => aggiorna({ casa: o.casa })} 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 ? "bg-card shadow-card text-foreground" : "text-muted-foreground", @@ -433,7 +435,7 @@ function GestioneEventi() { }) } 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 ? "bg-accent text-accent-foreground" : "bg-secondary text-muted-foreground", @@ -450,7 +452,7 @@ function GestioneEventi() { @@ -458,7 +460,7 @@ function GestioneEventi() { type="button" onClick={chiediConferma} 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 ? : null} Salva @@ -489,8 +491,8 @@ function GestioneEventi() { className="flex items-center gap-2 rounded-2xl bg-card p-3 shadow-card" >
-

{e.titolo}

-

+

{e.titolo}

+

{e.ora} · {e.luogo || "luogo da definire"}

@@ -513,7 +515,7 @@ function GestioneEventi() {
)) ) : ( -

+

Nessun evento in programma

)} @@ -521,7 +523,7 @@ function GestioneEventi() { @@ -544,14 +546,14 @@ function GestioneEventi() { type="button" onClick={conferma} 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 ? : null} Conferma @@ -573,14 +575,14 @@ function GestioneEventi() { type="button" onClick={() => daEliminare && rimuovi(daEliminare.id)} 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 ? : null} Elimina @@ -606,7 +608,7 @@ function Interruttore({ type="button" onClick={onClick} 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", )} > diff --git a/src/routes/evento.$id.tsx b/src/routes/evento.$id.tsx index 68a302e..a3bf16a 100644 --- a/src/routes/evento.$id.tsx +++ b/src/routes/evento.$id.tsx @@ -42,7 +42,7 @@ function EventoDetail() { if (!evento) { return (
-

Evento non trovato

+

Evento non trovato

); } @@ -58,14 +58,14 @@ function EventoDetail() {
-

+

Evento extra-campo

{evento.titolo}

-
+
{evento.ora} @@ -75,12 +75,12 @@ function EventoDetail() {
{evento.note ? ( -

+

{evento.note}

) : null} -
+
Conferme: {presentiVeri}/{convocatiEvento(evento, rosa).length}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 2c72149..83cde7c 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -9,6 +9,7 @@ import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo"; import { Reveal } from "@/components/motion/Reveal"; import { Barra } from "@/components/motion/Barra"; import { Numero } from "@/components/motion/Numero"; +import { ObiettivoDrawer } from "@/components/crapp/ObiettivoDrawer"; import { microcopyObiettivo, progressoObiettivo } from "@/lib/obiettivi"; import { useEventi, type Evento } from "@/lib/eventi"; 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" />
-

+

Ciao {nome.split(" ")[0]}

CRAP Volley

@@ -99,13 +100,13 @@ function Index() { >

{noi ? `${noi.pos}º` : "—"}

-

In classifica

+

In classifica

{noi ? `${noi.vinte}-${noi.perse}` : "—"}

-

Bilancio W-L

+

Bilancio W-L

{giocatore ? (
@@ -113,7 +114,7 @@ function Index() { {giocatore.streak}

-

Streak

+

Streak

) : null}
@@ -131,7 +132,7 @@ function Index() { azione={ Calendario @@ -140,7 +141,7 @@ function Index() { {prossimo ? ( ) : ( -

+

Nessun impegno in programma.

)} @@ -155,7 +156,7 @@ function Index() { return ; }) ) : ( -

+

Nient'altro da confermare: sei in pari.

)} @@ -170,7 +171,7 @@ function Index() { Storico @@ -182,8 +183,10 @@ function Index() { <>
-

CRAP Volley vs {ultima.avversario}

-

+

+ CRAP Volley vs {ultima.avversario} +

+

{ultima.casa ? "In casa" : "Trasferta"} {ultima.mvp ? ` · MVP ${ultima.mvp}` : " · MVP da votare"}

@@ -196,7 +199,7 @@ function Index() { {ultima.parziali.map((p, i) => ( {p[0]}-{p[1]} @@ -217,7 +220,7 @@ function Index() { ); })() ) : ( -

+

Nessun risultato disponibile.

)} @@ -230,27 +233,39 @@ function Index() { azione={ Tutti } > {obiettivo ? ( - -
- {obiettivo.emoji} {obiettivo.titolo} -
- -

- Siamo al —{" "} - {obiettivo.valore}/{obiettivo.target} {obiettivo.unita}. -

-

- {microcopyObiettivo(obiettivo)} -

-

{obiettivo.impatto}

-
+ + + ) : null} )} diff --git a/src/routes/partita-csi.$id.tsx b/src/routes/partita-csi.$id.tsx index 0409ef9..cbdd917 100644 --- a/src/routes/partita-csi.$id.tsx +++ b/src/routes/partita-csi.$id.tsx @@ -40,7 +40,7 @@ function PartitaCsiDetail() { if (!partita) { return (
-

Partita non trovata

+

Partita non trovata

); } @@ -69,7 +69,7 @@ function PartitaCsiDetail() { className="h-10 w-10" />
-

+

{partita.casa ? "In casa" : "Fuori casa"}

{partita.avversario}

@@ -93,7 +93,7 @@ function PartitaCsiDetail() { {partita.parziali.map(([noi, loro], i) => ( {noi}-{loro} diff --git a/src/routes/partita.$id.tsx b/src/routes/partita.$id.tsx index 714f966..b79972d 100644 --- a/src/routes/partita.$id.tsx +++ b/src/routes/partita.$id.tsx @@ -60,7 +60,7 @@ function PartitaDetail() { if (!evento) { return (
-

Partita non trovata

+

Partita non trovata

); } @@ -118,7 +118,7 @@ function PartitaDetail() { ) : null}
-

+

{casa ? "In casa" : "Fuori casa"} {evento.campionato ? " · Campionato" : " · Amichevole"}

@@ -126,7 +126,7 @@ function PartitaDetail() {
-
+
{evento.ora} @@ -136,12 +136,12 @@ function PartitaDetail() {
{evento.note ? ( -

+

{evento.note}

) : null} -
+
Conferme: {presentiVeri}/{convocati.length}
@@ -163,7 +163,7 @@ function PartitaDetail() {
- CRAP Volley + CRAP Volley {match.setNostri} - {match.setLoro} - {avversario} + {avversario}
{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" > {noi} - Set {i + 1} + + Set {i + 1} + {loro}
))} @@ -213,7 +215,7 @@ function PartitaDetail() { ) ) : (
-

+

La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il risultato.

@@ -248,13 +250,13 @@ function PartitaDetail() { ].map((t) => (

{t.v}

-

+

{t.l}

))}
-

+

Dettaglio giocatori (uso interno allenatori)

@@ -264,7 +266,7 @@ function PartitaDetail() { return (
#{g.numero} {g.nome} @@ -285,7 +287,7 @@ function PartitaDetail() { 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" > Esporta CSV diff --git a/src/routes/profilo.tsx b/src/routes/profilo.tsx index 9d4ce5b..a6af7e3 100644 --- a/src/routes/profilo.tsx +++ b/src/routes/profilo.tsx @@ -220,7 +220,7 @@ function CardAvatar({ id, fallback }: { id: string; fallback: string }) { @@ -312,11 +312,11 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) { type="button" onClick={cambiaNotifiche} 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" > Notifiche - + {supportate ? notifiche ? "Attive su questo dispositivo" @@ -339,11 +339,11 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) { type="button" onClick={cambiaEmail} 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" > Email - + {email.attive ? "Le notifiche arrivano anche via email" : "Nessuna email, solo notifiche in app"} @@ -363,7 +363,7 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) { {admin ? ( Dashboard amministratore @@ -373,7 +373,7 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) { href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=bug_report.yml" target="_blank" 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" > Segnala un bug @@ -382,17 +382,17 @@ function TabOpzioni({ giocatoreId }: { giocatoreId: string }) { href="https://github.com/ivancacciari1995-a11y/CRAPP/issues/new?template=feature_request.yml" target="_blank" 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" > Suggerisci una nuova funzionalità
-

CrAPP v{APP_VERSION}

+

CrAPP v{APP_VERSION}

{titolo}

-

{testo}

+

{testo}

{children}
@@ -138,7 +138,7 @@ function Scout() { @@ -156,7 +156,7 @@ function Scout() { if (ok) setControllo(true); 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"} @@ -313,7 +313,7 @@ function ScoutBoard({ <>
- + Scout live · Set {setCorrente}
@@ -321,14 +321,14 @@ function ScoutBoard({ type="button" onClick={annulla} 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" > Annulla @@ -337,14 +337,14 @@ function ScoutBoard({
-

CRAP Volley

+

CRAP Volley

{puntiNoi}

{setNostri}-{setLoro}

-

+

{avversario || "Avversario"}

{puntiLoro}

@@ -356,7 +356,7 @@ function ScoutBoard({ {setChiusi.map((p, i) => ( {p[0]}-{p[1]} @@ -366,7 +366,7 @@ function ScoutBoard({
-

+

1. Tocca il giocatore

@@ -384,10 +384,10 @@ function ScoutBoard({ )} >

{g.numero}

-

{g.nome}

+

{g.nome}

@@ -400,7 +400,7 @@ function ScoutBoard({

-

+

2. Tocca l'azione

@@ -426,7 +426,7 @@ function ScoutBoard({ type="button" onClick={() => registra(tipo)} 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, )} > @@ -438,11 +438,11 @@ function ScoutBoard({
-

+

Ultime azioni

{ultime.length === 0 ? ( -

Nessuna azione registrata.

+

Nessuna azione registrata.

) : (
    {ultime.map((a, i) => { @@ -451,14 +451,14 @@ function ScoutBoard({
  • {g ? `#${g.numero} ${g.nome}` : "Avversario"} @@ -474,14 +474,14 @@ function ScoutBoard({
    -