From 49d4d9de31fc5a881379608e75b8919fa7284041 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 24 Sep 2026 15:43:26 +0200 Subject: [PATCH 1/3] Documenta l'avviso certificati in scadenza in Home (DD-035) Specifica in profilo-giocatore.md: avviso giallo nei 7 giorni prima della scadenza e nero dopo, con i nomi di tutta la rosa per gli admin (non cliccabile) e solo il proprio per il giocatore interessato (apre Documenti). Calcolato al volo da certificato_scadenza: nessuna migration, nessuna push. Aggiornati roadmap, stato del progetto, allenatore e notifiche. Co-Authored-By: Claude Opus 5.5 (1M context) --- PROJECT_STATE.md | 7 +- docs/DESIGN_DECISIONS.md | 59 +++++++++++++ docs/ROADMAP.md | 4 + docs/modules/allenatore.md | 30 +++---- docs/modules/notifiche.md | 5 ++ docs/modules/profilo-giocatore.md | 132 +++++++++++++++++++++++++++++- 6 files changed, 219 insertions(+), 18 deletions(-) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 1a3ce87..1e95fbb 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -178,9 +178,10 @@ collega uno slot lo occupa anche in produzione, e va liberato da un admin. ## Prossimo sviluppo -Niente di assegnato: tutto quello che era in lavorazione è chiuso, tesseramento CSI incluso -(numero e data di tessera registrabili da `/admin`, migration `m8_tesseramento_csi`). Le voci -ancora aperte stanno in [docs/ROADMAP.md](docs/ROADMAP.md), sotto «Prossimo». +**Avviso certificati in scadenza** (24/09/2026, branch `develop`): specifica scritta in +[docs/modules/profilo-giocatore.md](docs/modules/profilo-giocatore.md#avviso-certificati), +decisione in DD-035, codice non ancora iniziato. Solo client: nessuna migration e nessuna +push. Le altre voci aperte stanno in [docs/ROADMAP.md](docs/ROADMAP.md), sotto «Prossimo». --- diff --git a/docs/DESIGN_DECISIONS.md b/docs/DESIGN_DECISIONS.md index 2b1778b..fb829fd 100644 --- a/docs/DESIGN_DECISIONS.md +++ b/docs/DESIGN_DECISIONS.md @@ -50,6 +50,7 @@ Serve a rispondere a domande del tipo: | [DD-032](#dd-032--la-stagione-csi-si-ricava-dalle-date-delle-gare) | Stagione CSI dalle date delle gare | | [DD-033](#dd-033--gestione-eventi-solo-calendario-giorno-fissato-dal-tocco) | Gestione eventi solo da calendario | | [DD-034](#dd-034--lallenatore-è-uno-slot-della-squadra-con-tipo-diverso-non-un-giocatore) | Ruolo allenatore | +| [DD-035](#dd-035--avviso-certificati-calcolato-in-home-visibile-solo-al-titolare-e-agli-admin) | Avviso certificati in Home | **In valutazione** @@ -1451,3 +1452,61 @@ collega uno slot di tipo allenatore: è quindi l'admin a concederlo, registrando **Riesame** Se arriva un secondo tipo di staff (dirigente, segreteria) o se i filtri sulla rosa diventano fonte di bug, passare alla tabella `staff` separata. + +### DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin + +**Data:** 24 settembre 2026 +**Stato:** Accettata (da implementare) + +**Contesto** +Il certificato medico ha una data di scadenza (`profili_giocatore.certificato_scadenza`), ma +oggi ci si accorge che è scaduto solo aprendo la tab Profili di `/admin`: spesso quando il +giocatore è già fuori regola. La richiesta è un avviso in Home, giallo nei 7 giorni prima +della scadenza e nero dopo, con i nomi dei giocatori, che sparisca da solo quando il +certificato viene rinnovato. Specifica completa in +[modules/profilo-giocatore.md](modules/profilo-giocatore.md#avviso-certificati). + +**Decisione** + +1. L'avviso si **calcola al volo** dalla data già salvata, con una funzione pura in + `profili-core.ts`: nessuna tabella, nessuno stato «avvisato», nessuna migration. +2. Lo vedono **solo gli admin**, con i nomi di tutta la rosa, e **il giocatore interessato**, + riferito solo a sé. L'allenatore e i compagni non lo vedono. +3. L'avviso dello staff **non è cliccabile**; quello personale apre + `/profilo?tab=documenti`. +4. Soglia fissa di **7 giorni** in una costante; il giorno della scadenza il certificato vale + ancora (come `statoScadenza()`). +5. **Niente push**: l'avviso vive solo in Home. + +**Alternative scartate** + +- Mostrarlo anche all'allenatore → il certificato è un dato sanitario che oggi l'allenatore + non vede (DD-034); servirebbe una funzione `SECURITY DEFINER` apposta per un'informazione + che all'allenatore non serve ad agire. +- Salvare gli avvisi in una tabella o nel centro notifiche (DD-030) → uno stato da tenere + allineato alla data, che si disallinea appena il giocatore corregge la scadenza; calcolarlo + è gratis e sempre giusto. +- Avviso dello staff cliccabile verso `/admin?tab=profili` → l'admin non può caricare il file + al posto del giocatore (DD-017), quindi il tocco non gli farebbe fare nulla in più, e + richiederebbe di aggiungere a `/admin` la lettura della tab dall'URL. +- Mostrare al massimo alcuni nomi e un «e altri N» → a inizio stagione ne scadono molti + insieme ed è proprio lì che l'elenco completo serve. +- Push quotidiana a chi ha il certificato in scadenza → servirebbe un cron, che oggi non + esiste per nessuna push (DD-025), e le push vanno riservate agli eventi importanti + ([EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md)). +- Includere i certificati mancanti → è un problema diverso (profilo incompleto), già coperto + dal widget «Completa il tuo profilo» e dalla tab Profili. + +**Conseguenze** + +- Nessuna query nuova: si riusa `useProfili()`, già in cache per Profilo e `/admin`, e le RLS + esistenti decidono quanti profili arrivano (tutti all'admin, il proprio al giocatore). +- Il rinnovo non richiede nessuna azione di chiusura: cambiata la data, l'avviso si aggiorna + da solo. +- Chi non apre l'app non viene avvisato: l'avviso raggiunge solo chi entra in Home. +- Un admin che è anche giocatore vede solo l'avviso dello staff, dove compare il suo nome. + +**Riesame** +Se gli avvisi in Home non bastano (certificati che scadono comunque senza essere rinnovati), +valutare una push, legata a un cron o a un invio manuale dell'admin come per i palloni. Se 7 +giorni si rivelano pochi per prenotare una visita, alzare la costante o renderla regolabile. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a2fccee..38be512 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -41,6 +41,10 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). ## Prossimo +- [ ] Avviso certificati in scadenza — in Home, giallo nei 7 giorni prima e nero dopo la + scadenza: agli admin con i nomi dei giocatori, al giocatore solo per il proprio + ([specifica](modules/profilo-giocatore.md#avviso-certificati)) + - [ ] Calendario ufficiale — i dati delle gare future arrivano già dal feed CSI, la pagina Campionato usa solo quelle giocate - [ ] Database esercizi diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index c876881..608c154 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -69,7 +69,8 @@ quando quei campi sono compilati. La Home dell'allenatore tiene ciò che riguarda la squadra e toglie ciò che riguarda il giocatore: restano classifica e bilancio, prossimo impegno e ultima partita; spariscono lo streak personale, il promemoria palloni, «Da confermare», l'obiettivo di squadra e il -«Colpo d'occhio» con le statistiche personali. +«Colpo d'occhio» con le statistiche personali. Non compare nemmeno l'avviso certificati: lo +vedono solo gli admin e il giocatore interessato (DD-035). ## Squadra @@ -148,19 +149,20 @@ giocatore). ## Permessi (riepilogo) -| Azione | Giocatore | Allenatore | Admin | -| ----------------------------------------------- | --------- | ------------------ | ------------------ | -| Modificare nome e cognome | no | sì, i propri | tutti | -| Dati personali e foto profilo | i propri | i propri (ridotti) | tutti (non i file) | -| Documento, certificato, foto tessera | i propri | — | lettura/download | -| Creare/modificare/eliminare eventi, convocati | no | sì | sì | -| Sollecitare le presenze | no | sì | sì | -| Promemoria palloni, notifica personalizzata | no | no | sì | -| Rispondere alle presenze, votare, essere votato | sì | no | sì (se giocatore) | -| Tab Stagione e Badge del profilo | sì | no | sì | -| Tab Obiettivi e Badge di Squadra | sì | no | sì | -| Badge e classifiche/dati sulle cacche | sì | no | sì | -| Dashboard `/admin` | no | no | sì | +| Azione | Giocatore | Allenatore | Admin | +| ----------------------------------------------- | ---------- | ------------------ | ------------------ | +| Modificare nome e cognome | no | sì, i propri | tutti | +| Dati personali e foto profilo | i propri | i propri (ridotti) | tutti (non i file) | +| Documento, certificato, foto tessera | i propri | — | lettura/download | +| Avviso certificati in Home | il proprio | no | tutta la rosa | +| Creare/modificare/eliminare eventi, convocati | no | sì | sì | +| Sollecitare le presenze | no | sì | sì | +| Promemoria palloni, notifica personalizzata | no | no | sì | +| Rispondere alle presenze, votare, essere votato | sì | no | sì (se giocatore) | +| Tab Stagione e Badge del profilo | sì | no | sì | +| Tab Obiettivi e Badge di Squadra | sì | no | sì | +| Badge e classifiche/dati sulle cacche | sì | no | sì | +| Dashboard `/admin` | no | no | sì | ## Implementazione diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md index d404825..9778260 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -18,6 +18,11 @@ indipendenti: - **Notifiche smart** — notifiche locali mostrate solo ad app aperta, generate da badge, serie e obiettivi appena raggiunti; non è un canale push separato. +Gli avvisi fissi in Home (turno palloni, certificati in scadenza) non passano da nessuno dei +due: sono card calcolate dai dati, che spariscono da sole quando la condizione finisce. In +particolare il certificato in scadenza **non** genera push né voci nel centro notifiche +(DD-035, specifica in [profilo-giocatore.md](profilo-giocatore.md#avviso-certificati)). + --- ## Dati diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index 0b100cc..f2ff735 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -64,7 +64,7 @@ Se il profilo non è completo compare automaticamente un widget di completamento ## Home -Il giocatore visualizza un widget dedicato. +Il giocatore visualizza un widget dedicato e, quando serve, l'avviso sul certificato medico. ### Completa il tuo profilo @@ -80,6 +80,128 @@ Quando tutte le sezioni sono complete il widget scompare automaticamente. Il tap apre Profilo sulla sottosezione **Documenti** (`/profilo?tab=documenti`), non sulla tab Stagione. +### Avviso certificati + +**Stato:** da implementare (DD-035). + +Un avviso in Home segnala i certificati medici in scadenza o scaduti, così nessuno se ne +accorge quando il giocatore è già fuori regola. Si calcola al volo dalla data di scadenza +già salvata: niente tabella, niente migration, niente push. + +#### Chi lo vede + +| Utente | Cosa vede | Al tocco | +| --------------------------- | --------------------------------------------------------------- | ----------------------------- | +| Admin | l'avviso **dello staff**: i nomi di tutti i giocatori coinvolti | niente, non è cliccabile | +| Giocatore interessato | l'avviso **personale**: solo il proprio certificato | apre `/profilo?tab=documenti` | +| Admin che è anche giocatore | solo l'avviso dello staff, dove compare già il suo nome | niente | +| Altri giocatori | niente | — | +| Allenatore | niente: non vede i certificati altrui e non ne ha uno (DD-034) | — | + +L'avviso dello staff non è cliccabile, come quello dei palloni: l'admin non può caricare il +certificato al posto del giocatore (DD-017), quindi può solo sollecitarlo, e il giocatore +riceve già il proprio avviso. Quello personale porta invece dove si carica il certificato +nuovo, come il widget «Completa il tuo profilo». + +I certificati sono dati sanitari: fuori da `/admin` li legge solo il titolare (DD-016). +Nessuna policy cambia, perché `useProfili()` restituisce già tutti i profili a un admin e il +solo proprio a un giocatore. + +#### Quando compare + +`giorni` è la differenza in giorni di calendario tra la scadenza e oggi (ora locale), soglia +`GIORNI_AVVISO_CERTIFICATO = 7`. + +| Condizione | Stato | Avviso | +| -------------------- | ----------- | ------------------ | +| `giorni > 7` | valido | nessuno | +| `0 ≤ giorni ≤ 7` | in scadenza | giallo (`warning`) | +| `giorni < 0` | scaduto | nero (`primary`) | +| data o file mancanti | mancante | nessuno | + +- Il giorno della scadenza il certificato vale ancora, coerente con `statoScadenza()`: il + giocatore compare nel giallo con «scade oggi» e passa al nero dal giorno dopo. +- L'avviso nero resta finché il giocatore non aggiorna la data: non c'è un limite di tempo. +- Il certificato **mancante** non genera avviso: è un problema diverso, già visibile nella + tab Profili di `/admin` e nel widget di completamento del giocatore. +- Sono esclusi i giocatori disattivati e gli allenatori: conta solo la rosa (`inRosa()`, + cioè `attivo` e `tipo = 'giocatore'`). + +#### Testi + +| Avviso | Titolo (staff / personale) | Riga | +| ----------------- | -------------------------- | ------------------------------------------------------------------------ | +| Giallo, staff | «Certificati in scadenza» | «Mario Rossi — scade tra 5 giorni», «… — scade domani», «… — scade oggi» | +| Nero, staff | «Certificati scaduti» | «Mario Rossi — scaduto il 12/09/2026» | +| Giallo, personale | «Certificato in scadenza» | «Il tuo certificato medico scade tra 5 giorni» / «domani» / «oggi» | +| Nero, personale | «Certificato scaduto» | «Il tuo certificato medico è scaduto il 12/09/2026: caricane uno nuovo» | + +Il numero di giorni scende da solo ogni giorno (7, 6, 5… domani, oggi): l'avviso non +conserva niente, rilegge la data a ogni apertura della Home. + +#### Più giocatori + +- Più giocatori nello stesso stato stanno in un **unico avviso**, una riga ciascuno, e + compaiono **tutti**: nessun limite al numero di nomi, anche a inizio stagione quando ne + scadono molti insieme. +- Se ci sono sia certificati in scadenza sia scaduti compaiono **due avvisi**: prima quello + nero, poi quello giallo. +- Ordine delle righe: dalla scadenza più vicina nel giallo, dal certificato scaduto da più + tempo nel nero; a parità di data, alfabetico per cognome e poi per nome. L'ordine è + stabile, quindi l'elenco non si rimescola tra un'apertura e l'altra. + +#### Aspetto e posizione + +- Stessa forma del banner palloni (`PromemoriaPalloni`): card arrotondata a tutta larghezza + con icona e titolo in `font-display` maiuscolo, righe in testo piccolo sotto. +- Giallo: sfondo `bg-warning`, testo `text-warning-foreground`. Nero: sfondo `bg-primary`, + testo `text-primary-foreground`. L'app è solo chiara (DD-022), quindi il nero non rischia + di confondersi con lo sfondo. +- In Home sta subito **sotto il banner palloni** e sopra «Completa il tuo profilo»: prima + ciò che scade oggi, poi ciò che manca. + +#### Rinnovo + +Il giocatore rinnova dal proprio profilo, sottosezione Documenti: carica il file nuovo e +aggiorna la data. Il salvataggio aggiorna la cache dei profili (`setQueryData`), quindi il +suo avviso personale sparisce subito. Nell'avviso dello staff il nome sparisce alla lettura +successiva dei profili da parte dell'admin (cache di 30 minuti, `useProfili()`), senza +nessuna azione da parte sua. Quando non resta nessuno, l'avviso non compare più. + +Basta aggiornare la data: l'avviso guarda solo quella (e la presenza di un file). Che il +file caricato sia davvero quello nuovo resta un controllo dell'admin, come oggi. Anche l'admin +può correggere la data dalla scheda del giocatore (DD-017, non il file): l'avviso segue la +data, chiunque l'abbia scritta. + +#### Caricamento + +Finché ruoli e profili non sono arrivati, l'avviso non compare: così un admin che è anche +giocatore non vede per un attimo l'avviso personale prima di quello dello staff. Se la +lettura dei profili fallisce l'avviso semplicemente non compare, senza messaggio d'errore: +la Home non deve rompersi per un dato accessorio. + +#### Implementazione + +| Pezzo | Ruolo | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `GIORNI_AVVISO_CERTIFICATO` (`profili-core.ts`) | la soglia, 7 | +| `giorniAllaScadenza(scadenza, oggi)` | differenza in giorni di calendario tra due date `AAAA-MM-GG`, negativa se scaduto | +| `avvisiCertificati(rosa, profili, oggi)` | funzione pura: restituisce `{ scaduti, inScadenza }`, ognuno una lista ordinata di `{ giocatoreId, nome, cognome, scadenza, giorni }` | +| `testoScadenza(giorni)` | «scade oggi» / «scade domani» / «scade tra N giorni» | +| `src/components/crapp/AvvisoCertificati.tsx` | legge `useGiocatoriSquadra()`, `useProfili()`, `useIsAdmin()` e l'utente corrente; sceglie avviso dello staff o personale e disegna le card | +| `src/routes/index.tsx` | monta `` subito dopo `` | + +- "Oggi" viene da `oggiISO()` (`palloni-core.ts`), in ora locale. Non da + `new Date().toISOString()`, che è in UTC e tra mezzanotte e le 2 darebbe il giorno prima. +- Nessuna query nuova: `useProfili()` ha la stessa chiave di cache usata da Profilo e + `/admin`, quindi al massimo una lettura di `profili_giocatore` per sessione (vedi + [EFFICIENZA_CLOUD.md](../EFFICIENZA_CLOUD.md)). Per l'allenatore il componente non legge + i profili. +- Test in `test/unit/profili-core.test.ts`: soglia (8 giorni no, 7 sì, 0 sì, −1 scaduto), + certificato senza file o senza data escluso, giocatore disattivato e allenatore esclusi, + ordinamento (per data, poi alfabetico), testi al singolare e al plurale, cambio di mese e + di anno nel calcolo dei giorni. + ## Profilo Il profilo viene suddiviso in sette aree. @@ -132,6 +254,10 @@ Upload. Il giocatore può aggiornare liberamente sia la data sia il file. +Quando mancano 7 giorni o meno alla scadenza, o il certificato è scaduto, il giocatore +riceve un avviso nella propria Home e compare in quello degli admin (vedi +[Avviso certificati](#avviso-certificati)). + Lo storico non viene mantenuto nella prima versione. ### Foto tessera @@ -255,6 +381,9 @@ Quando tutte le sezioni risultano complete il profilo raggiunge il 100%. dati, modificare dati squadra e dati personali di chiunque e scollegare un account (DD-017). Non carica file al posto di altri. +**Avviso certificati** — il giocatore vede solo il proprio, l'admin i nomi di tutta la rosa, +nessun altro ne vede (vedi [Avviso certificati](#avviso-certificati)). + ## Versione 1 - Profilo giocatore @@ -269,6 +398,7 @@ Non carica file al posto di altri. ## Versioni future - Storico certificati medici +- Avviso certificati anche via push, o con una soglia regolabile dall'admin (DD-035, Riesame) - Gestione documenti aggiuntivi - Consensi privacy - Firma digitale From a51cdb43e49065d2c202cc5de1c5c8c8da4d44c6 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 27 Sep 2026 14:06:18 +0200 Subject: [PATCH 2/3] Implementa l'avviso certificati in scadenza in Home (DD-035) Calcolato al volo da certificato_scadenza in profili-core.ts: nessuna tabella, nessuna migration, nessuna push. Lo vedono solo gli admin (nomi di tutta la rosa, non cliccabile) e il giocatore interessato (solo il proprio, apre Documenti). Montato in Home subito dopo il promemoria palloni. Co-Authored-By: Claude Sonnet 5 --- PROJECT_STATE.md | 9 +- docs/modules/profilo-giocatore.md | 2 +- src/components/crapp/AvvisoCertificati.tsx | 113 +++++++++++++++++++ src/lib/profili-core.ts | 72 ++++++++++++- src/lib/ruoli.ts | 14 ++- src/routes/index.tsx | 3 + test/unit/profili-core.test.ts | 119 +++++++++++++++++++++ 7 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 src/components/crapp/AvvisoCertificati.tsx diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 1e95fbb..c39731a 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -178,10 +178,11 @@ collega uno slot lo occupa anche in produzione, e va liberato da un admin. ## Prossimo sviluppo -**Avviso certificati in scadenza** (24/09/2026, branch `develop`): specifica scritta in -[docs/modules/profilo-giocatore.md](docs/modules/profilo-giocatore.md#avviso-certificati), -decisione in DD-035, codice non ancora iniziato. Solo client: nessuna migration e nessuna -push. Le altre voci aperte stanno in [docs/ROADMAP.md](docs/ROADMAP.md), sotto «Prossimo». +**Avviso certificati in scadenza** (27/09/2026, branch `develop`): codice scritto secondo la +specifica in [docs/modules/profilo-giocatore.md](docs/modules/profilo-giocatore.md#avviso-certificati) +(decisione DD-035), non ancora rilasciato in una versione. Solo client: nessuna migration e +nessuna push. Le altre voci aperte stanno in [docs/ROADMAP.md](docs/ROADMAP.md), sotto +«Prossimo». --- diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index f2ff735..785fc6f 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -82,7 +82,7 @@ sulla tab Stagione. ### Avviso certificati -**Stato:** da implementare (DD-035). +**Stato:** implementato (DD-035). Un avviso in Home segnala i certificati medici in scadenza o scaduti, così nessuno se ne accorge quando il giocatore è già fuori regola. Si calcola al volo dalla data di scadenza diff --git a/src/components/crapp/AvvisoCertificati.tsx b/src/components/crapp/AvvisoCertificati.tsx new file mode 100644 index 0000000..94b4ac9 --- /dev/null +++ b/src/components/crapp/AvvisoCertificati.tsx @@ -0,0 +1,113 @@ +import { AlertCircle } from "lucide-react"; +import { Link } from "@tanstack/react-router"; +import { useProfili } from "@/lib/profili"; +import { + avvisiCertificati, + formatDataBreve, + testoScadenza, + type AvvisoCertificato, +} from "@/lib/profili-core"; +import { useGiocatoriSquadra } from "@/lib/giocatori-squadra"; +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"; + 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" + : "bg-primary text-primary-foreground" + } ${cliccabile ? "premi" : ""}`; + const contenuto = ( + <> +

+ {titolo} +

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

+ {r} +

+ ))} + + ); + return cliccabile ? ( + + {contenuto} + + ) : ( +
{contenuto}
+ ); +} + +/** Avviso certificati in scadenza/scaduti (DD-035): calcolato al volo, niente push. */ +export function AvvisoCertificati() { + const admin = useIsAdmin(); + const ruoliPronti = useRuoliPronti(); + const base = useGiocatoreBase(); + const { righe: rosa } = useGiocatoriSquadra(); + const { profili, isPending } = useProfili(); + + if (!ruoliPronti || isPending) return null; + + const oggi = oggiISO(); + + if (admin) { + const { scaduti, inScadenza } = avvisiCertificati(rosa, profili, oggi); + if (scaduti.length === 0 && inScadenza.length === 0) return null; + return ( + <> + {scaduti.length > 0 ? ( + rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} + /> + ) : null} + {inScadenza.length > 0 ? ( + rigaStaff(a, testoScadenza(a.giorni)))} + /> + ) : null} + + ); + } + + if (!base) return null; + const { scaduti, inScadenza } = avvisiCertificati([base], profili, oggi); + const mio = scaduti[0] ?? inScadenza[0]; + if (!mio) return null; + + return mio.giorni < 0 ? ( + + ) : ( + + ); +} + +function rigaStaff(a: AvvisoCertificato, dettaglio: string): string { + return `${a.nome} ${a.cognome} — ${dettaglio}`; +} diff --git a/src/lib/profili-core.ts b/src/lib/profili-core.ts index 20d8e86..aff48d6 100644 --- a/src/lib/profili-core.ts +++ b/src/lib/profili-core.ts @@ -1,5 +1,5 @@ import { rigaCsv } from "./scout-export"; -import { nomeCompleto, type GiocatoreSquadra } from "./giocatori-squadra"; +import { inRosa, nomeCompleto, type GiocatoreSquadra } from "./giocatori-squadra"; /** * Profilo amministrativo di un giocatore (DD-016). I file veri stanno nel bucket privato @@ -215,3 +215,73 @@ export function csvTesseramento( export function etichettaGiocatore(g: GiocatoreSquadra): string { return `#${g.numero} ${nomeCompleto(g)}`; } + +/** Soglia dell'avviso certificati in Home (DD-035). */ +export const GIORNI_AVVISO_CERTIFICATO = 7; + +/** Giorni di calendario tra due date `AAAA-MM-GG` (ora locale): negativo se `scadenza` è passata. */ +export function giorniAllaScadenza(scadenza: string, oggi: string): number { + const a = new Date(`${scadenza}T00:00:00`); + const b = new Date(`${oggi}T00:00:00`); + return Math.round((a.getTime() - b.getTime()) / 86_400_000); +} + +/** Data in formato `GG/MM/AAAA`, per i testi dell'avviso certificati. */ +export function formatDataBreve(iso: string): string { + return new Date(`${iso}T00:00:00`).toLocaleDateString("it-IT", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +/** Riga dell'avviso certificati per un giocatore (DD-035). */ +export function testoScadenza(giorni: number): string { + if (giorni === 0) return "scade oggi"; + if (giorni === 1) return "scade domani"; + return `scade tra ${giorni} giorni`; +} + +export type AvvisoCertificato = { + giocatoreId: string; + nome: string; + cognome: string; + scadenza: string; + giorni: number; +}; + +function confrontaAvvisi(a: AvvisoCertificato, b: AvvisoCertificato): number { + return a.giorni - b.giorni || a.cognome.localeCompare(b.cognome) || a.nome.localeCompare(b.nome); +} + +/** + * Certificati in scadenza o scaduti nella rosa (DD-035): calcolato al volo da + * `certificato_scadenza`, niente stato salvato. Esclude chi non è in rosa e chi non ha + * ancora un certificato caricato (è un problema diverso, non un avviso di scadenza). + */ +export function avvisiCertificati( + rosa: GiocatoreSquadra[], + profili: Record, + oggi: string, +): { scaduti: AvvisoCertificato[]; inScadenza: AvvisoCertificato[] } { + const scaduti: AvvisoCertificato[] = []; + const inScadenza: AvvisoCertificato[] = []; + for (const g of rosa) { + if (!inRosa(g)) continue; + const p = profili[g.id]; + if (!p?.certificatoScadenza || !p.certificatoPath) continue; + const giorni = giorniAllaScadenza(p.certificatoScadenza, oggi); + if (giorni > GIORNI_AVVISO_CERTIFICATO) continue; + const avviso: AvvisoCertificato = { + giocatoreId: g.id, + nome: g.nome, + cognome: g.cognome, + scadenza: p.certificatoScadenza, + giorni, + }; + (giorni < 0 ? scaduti : inScadenza).push(avviso); + } + scaduti.sort(confrontaAvvisi); + inScadenza.sort(confrontaAvvisi); + return { scaduti, inScadenza }; +} diff --git a/src/lib/ruoli.ts b/src/lib/ruoli.ts index 4ae1dbf..2398c6d 100644 --- a/src/lib/ruoli.ts +++ b/src/lib/ruoli.ts @@ -24,15 +24,23 @@ async function fetchRuoli(utenteId: string | null): Promise { return (data ?? []).map((r) => r.role as Ruolo); } -function useRuoli(): Ruolo[] { +function useRuoliQuery() { const { utenteId } = useSessione(); // Il ruolo cambia solo quando un admin lo assegna: una lettura per sessione basta. - const query = useQuery({ + return useQuery({ queryKey: [...RUOLI_KEY, utenteId], queryFn: () => fetchRuoli(utenteId), staleTime: 30 * 60_000, }); - return query.data ?? []; +} + +function useRuoli(): Ruolo[] { + return useRuoliQuery().data ?? []; +} + +/** L'avviso certificati (DD-035) aspetta i ruoli: niente flash dell'avviso sbagliato. */ +export function useRuoliPronti(): boolean { + return !useRuoliQuery().isPending; } export function useIsAdmin(): boolean { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 46b1f07..2c72149 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -3,6 +3,7 @@ import { cn } from "@/lib/utils"; import { Flame, ChevronRight } from "lucide-react"; import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard"; import { PromemoriaPalloni } from "@/components/crapp/PromemoriaPalloni"; +import { AvvisoCertificati } from "@/components/crapp/AvvisoCertificati"; import { Card, LinkProfilo, Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits"; import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo"; import { Reveal } from "@/components/motion/Reveal"; @@ -120,6 +121,8 @@ function Index() { + +
= { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-09-25", certificatoPath: "p1" }, // in scadenza, 5 giorni + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-09-10", certificatoPath: "p2" }, // scaduto da 10 giorni + g3: { ...vuoto, giocatoreId: "g3", certificatoScadenza: "2026-10-31", certificatoPath: "p3" }, // valido, fuori soglia + g4: { ...vuoto, giocatoreId: "g4", certificatoScadenza: "2026-09-21", certificatoPath: "p4" }, // disattivato: escluso + g5: { ...vuoto, giocatoreId: "g5", certificatoScadenza: "2026-09-21", certificatoPath: "p5" }, // allenatore: escluso +}; +const avvisi = avvisiCertificati(rosaCertificati, profiliCertificati, oggiTest); +assert.deepEqual( + avvisi.scaduti.map((a) => a.giocatoreId), + ["g2"], +); +assert.deepEqual( + avvisi.inScadenza.map((a) => a.giocatoreId), + ["g1"], +); + +assert.equal( + avvisiCertificati( + [{ ...squadra[0]!, id: "g6" }], + { g6: { ...vuoto, giocatoreId: "g6", certificatoScadenza: null, certificatoPath: null } }, + oggiTest, + ).inScadenza.length, + 0, + "certificato mancante: nessun avviso", +); + +const dueInScadenza: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-09-27", certificatoPath: "p1" }, // 7 giorni, più lontano + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-09-21", certificatoPath: "p2" }, // 1 giorno, più vicino +}; +const ordinati = avvisiCertificati( + [ + { ...squadra[0]!, id: "g1", nome: "Ivan", cognome: "Cacciari" }, + { ...squadra[0]!, id: "g2", nome: "Anna", cognome: "Bruni" }, + ], + dueInScadenza, + oggiTest, +).inScadenza.map((a) => a.giocatoreId); +assert.deepEqual(ordinati, ["g2", "g1"], "la scadenza più vicina viene prima"); + +const pariGiorni: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-09-25", certificatoPath: "p1" }, + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-09-25", certificatoPath: "p2" }, +}; +const alfabetico = avvisiCertificati( + [ + { ...squadra[0]!, id: "g1", nome: "Ivan", cognome: "Zeta" }, + { ...squadra[0]!, id: "g2", nome: "Anna", cognome: "Alfa" }, + ], + pariGiorni, + oggiTest, +).inScadenza.map((a) => a.giocatoreId); +assert.deepEqual(alfabetico, ["g2", "g1"], "a parità di data, ordine alfabetico per cognome"); + +// soglia esatta (DD-035): 8 giorni fuori, 7 e 0 dentro +const rosaSoglia: GiocatoreSquadra[] = [ + { ...squadra[0]!, id: "g1", nome: "Ivan", cognome: "Cacciari" }, + { ...squadra[0]!, id: "g2", nome: "Anna", cognome: "Bruni" }, + { ...squadra[0]!, id: "g3", nome: "Marco", cognome: "Verdi" }, +]; +const profiliSoglia: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-09-28", certificatoPath: "p1" }, // 8 giorni: fuori + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-09-27", certificatoPath: "p2" }, // 7 giorni: dentro + g3: { ...vuoto, giocatoreId: "g3", certificatoScadenza: "2026-09-20", certificatoPath: "p3" }, // 0 giorni: dentro +}; +const soglia = avvisiCertificati(rosaSoglia, profiliSoglia, oggiTest); +assert.deepEqual( + soglia.inScadenza.map((a) => a.giocatoreId), + ["g3", "g2"], + "8 giorni resta valido, 7 e 0 generano avviso", +); +assert.equal(soglia.scaduti.length, 0); + +// ordinamento tra scaduti: il più vecchio prima, poi alfabetico a parità di data +const rosaScaduti: GiocatoreSquadra[] = [ + { ...squadra[0]!, id: "g1", nome: "Ivan", cognome: "Zeta" }, + { ...squadra[0]!, id: "g2", nome: "Anna", cognome: "Alfa" }, + { ...squadra[0]!, id: "g3", nome: "Marco", cognome: "Verdi" }, +]; +const profiliScaduti: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-09-01", certificatoPath: "p1" }, // scaduto da 19 giorni + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-09-01", certificatoPath: "p2" }, // pari data di g1 + g3: { ...vuoto, giocatoreId: "g3", certificatoScadenza: "2026-09-19", certificatoPath: "p3" }, // scaduto da 1 giorno +}; +const scadutiOrdinati = avvisiCertificati(rosaScaduti, profiliScaduti, oggiTest).scaduti.map( + (a) => a.giocatoreId, +); +assert.deepEqual( + scadutiOrdinati, + ["g2", "g1", "g3"], + "il più vecchio prima, poi alfabetico a parità di data (Alfa prima di Zeta)", +); + console.log("profili-core: ok"); From 0a2112a9f37b10e34901ce83bdedec09dba77fa2 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 27 Sep 2026 14:09:02 +0200 Subject: [PATCH 3/3] Porta la versione a 1.1.0 per l'avviso certificati (DD-035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiunta la sezione [1.1.0] nel changelog, spostata la voce da «Prossimo» a «Fatto» nella roadmap e chiuso lo stato di DD-035 come implementata. Co-Authored-By: Claude Sonnet 5 --- PROJECT_STATE.md | 10 ++++------ docs/CHANGELOG.md | 10 ++++++++++ docs/DESIGN_DECISIONS.md | 2 +- docs/ROADMAP.md | 7 +++---- package.json | 2 +- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index c39731a..6aab49e 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,6 +1,6 @@ # Project State -Ultimo aggiornamento: 24/09/2026 +Ultimo aggiornamento: 27/09/2026 ## Stato generale @@ -178,11 +178,9 @@ collega uno slot lo occupa anche in produzione, e va liberato da un admin. ## Prossimo sviluppo -**Avviso certificati in scadenza** (27/09/2026, branch `develop`): codice scritto secondo la -specifica in [docs/modules/profilo-giocatore.md](docs/modules/profilo-giocatore.md#avviso-certificati) -(decisione DD-035), non ancora rilasciato in una versione. Solo client: nessuna migration e -nessuna push. Le altre voci aperte stanno in [docs/ROADMAP.md](docs/ROADMAP.md), sotto -«Prossimo». +Niente di assegnato: l'avviso certificati in scadenza (DD-035) è implementato e versione +1.1.0 (vedi `docs/CHANGELOG.md`). Le voci ancora aperte stanno in +[docs/ROADMAP.md](docs/ROADMAP.md), sotto «Prossimo». --- diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9802c5e..cdee1ee 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,16 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +## [1.1.0] - 2026-09-27 + +### Aggiunto + +- **Avviso certificati in scadenza** — in Home, giallo nei 7 giorni prima e nero dopo la + scadenza del certificato medico. Gli admin vedono i nomi di tutta la rosa (non + cliccabile); il giocatore interessato vede solo il proprio e il tap apre + `/profilo?tab=documenti`. Calcolato al volo da `certificato_scadenza`: nessuna migration, + nessuna push (DD-035). + ## [1.0.0] - 2026-09-24 Prima versione stabile. Oltre al ruolo allenatore riunisce il lavoro fatto dalla 0.9.2: diff --git a/docs/DESIGN_DECISIONS.md b/docs/DESIGN_DECISIONS.md index fb829fd..4a7bea2 100644 --- a/docs/DESIGN_DECISIONS.md +++ b/docs/DESIGN_DECISIONS.md @@ -1456,7 +1456,7 @@ fonte di bug, passare alla tabella `staff` separata. ### DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin **Data:** 24 settembre 2026 -**Stato:** Accettata (da implementare) +**Stato:** Accettata (implementata, versione 1.1.0) **Contesto** Il certificato medico ha una data di scadenza (`profili_giocatore.certificato_scadenza`), ma diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 38be512..2f329a1 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -38,13 +38,12 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). calcolata dal CSI - [x] Ruolo allenatore — gestisce gli eventi e sollecita le presenze, profilo ridotto senza stagione e badge ([specifica](modules/allenatore.md), DD-034) +- [x] Avviso certificati in scadenza — in Home, giallo nei 7 giorni prima e nero dopo la + scadenza: agli admin con i nomi dei giocatori, al giocatore solo per il proprio + ([specifica](modules/profilo-giocatore.md#avviso-certificati), DD-035) ## Prossimo -- [ ] Avviso certificati in scadenza — in Home, giallo nei 7 giorni prima e nero dopo la - scadenza: agli admin con i nomi dei giocatori, al giocatore solo per il proprio - ([specifica](modules/profilo-giocatore.md#avviso-certificati)) - - [ ] Calendario ufficiale — i dati delle gare future arrivano già dal feed CSI, la pagina Campionato usa solo quelle giocate - [ ] Database esercizi diff --git a/package.json b/package.json index ccf20f1..74513ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "crapp", - "version": "1.0.0", + "version": "1.1.0", "private": true, "sideEffects": false, "type": "module",