From 4b5ffc666452a4b916be336d92ccda0e5bd611fb Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 1 Oct 2026 14:21:25 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Avviso=20certificati:=20il=20giocatore=20?= =?UTF-8?q?=C3=A8=20avvisato=2030=20giorni=20prima=20(DD-041)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il giocatore vede l'avviso giallo da 30 giorni prima della scadenza del certificato medico; per lo staff resta 7 giorni, nero dopo la scadenza per tutti. L'admin che è anche giocatore vede il proprio avviso personale oltre a quello dello staff, da cui è escluso il suo nome. La scelta degli avvisi per utente è in avvisiCertificatiUtente() (profili-core) con test; documentati DD-041, DD-035, modulo e changelog. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 6 ++ docs/decisions/DD-035.md | 4 +- docs/decisions/DD-041.md | 39 ++++++++++ docs/decisions/README.md | 81 +++++++++---------- docs/modules/profilo-giocatore.md | 67 +++++++++------- src/components/crapp/AvvisoCertificati.tsx | 91 +++++++++++----------- src/lib/profili-core.ts | 38 ++++++++- test/unit/profili-core.test.ts | 76 ++++++++++++++++++ 8 files changed, 285 insertions(+), 117 deletions(-) create mode 100644 docs/decisions/DD-041.md diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e74a0e4..d7b7053 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,12 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +### Modificato + +- **Avviso certificati: giocatore a 30 giorni** — il giocatore vede l'avviso giallo sul proprio + certificato medico da un mese prima della scadenza (prima 7 giorni). Per gli admin non cambia + nulla: 7 giorni prima in giallo, nero dopo la scadenza (DD-041). + ## [1.2.1] - 2026-09-30 Turno palloni e sollecito presenze partono anche da soli, a orari fissi prima dell'evento, e i testi di diff --git a/docs/decisions/DD-035.md b/docs/decisions/DD-035.md index 31c474c..64875cd 100644 --- a/docs/decisions/DD-035.md +++ b/docs/decisions/DD-035.md @@ -1,7 +1,7 @@ # DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin **Data:** 24 settembre 2026 -**Stato:** Accettata (implementata, versione 1.1.0) +**Stato:** Accettata (implementata, versione 1.1.0). La soglia del giocatore è stata alzata a 30 giorni da [DD-041](DD-041.md); quella dello staff resta 7. **Contesto** Il certificato medico ha una data di scadenza (`profili_giocatore.certificato_scadenza`), ma @@ -49,7 +49,7 @@ certificato viene rinnovato. Specifica completa in - 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. +- ~~Un admin che è anche giocatore vede solo l'avviso dello staff~~ → superato da DD-041: vede anche il proprio avviso personale. **Riesame** Se gli avvisi in Home non bastano (certificati che scadono comunque senza essere rinnovati), diff --git a/docs/decisions/DD-041.md b/docs/decisions/DD-041.md new file mode 100644 index 0000000..688ea4f --- /dev/null +++ b/docs/decisions/DD-041.md @@ -0,0 +1,39 @@ +# DD-041 — Avviso certificati: il giocatore è avvisato un mese prima dello staff + +**Data:** 1 ottobre 2026 +**Stato:** Accettata (modifica la soglia di [DD-035](DD-035.md) per il solo giocatore) + +**Contesto** +Con DD-035 giocatore e admin vedono l'avviso negli stessi 7 giorni prima della scadenza. Per +il giocatore sono pochi: prenotare una visita di idoneità richiede più tempo (era il rischio +indicato nel Riesame di DD-035). All'admin invece un avviso così anticipato riempirebbe la +Home di nomi che non può ancora sollecitare con urgenza. + +**Decisione** + +1. Il **giocatore** vede l'avviso giallo da **30 giorni** prima della scadenza + (`GIORNI_AVVISO_CERTIFICATO_GIOCATORE`). +2. L'**admin** resta a **7 giorni** prima (`GIORNI_AVVISO_CERTIFICATO`), invariato. +3. Dopo la scadenza l'avviso è **nero** per entrambi, invariato. +4. L'**admin che è anche giocatore** vede il proprio avviso **personale** (30 giorni, + cliccabile) oltre a quello dello staff, dal quale il suo nome è tolto per non duplicarlo. +5. Nessun altro aspetto di DD-035 cambia: calcolo al volo, niente push, stessi destinatari. + +Esempio: scadenza il 5 ottobre 2026 → dal 5 settembre l'avviso giallo compare solo al +giocatore; dal 28 settembre compare anche agli admin; dal 6 ottobre è nero per entrambi. + +**Alternative scartate** + +- «Un mese» come mese di calendario → la soglia varierebbe tra 28 e 31 giorni; 30 giorni fissi + sono prevedibili e coincidono con l'esempio della richiesta. +- Soglia unica a 30 giorni anche per l'admin → elenco dello staff pieno con settimane di + anticipo, senza nulla da fare per l'admin che non può caricare il file (DD-017). + +**Conseguenze** + +- Il giocatore vede il conto alla rovescia «scade tra N giorni» con N fino a 30. +- `avvisiCertificati()` riceve la soglia come parametro (default 7, cioè lo staff). + +**Riesame** +Se 30 giorni risultano ancora pochi o troppi, cambiare la costante; se serve una soglia +regolabile dall'admin, vedi il Riesame di DD-035. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index db9f954..de708db 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -46,46 +46,47 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand **Accettate** -| ID | Titolo | -| ------------------- | ------------------------------------- | -| [DD-001](DD-001.md) | Indipendenza da Lovable | -| [DD-002](DD-002.md) | Sviluppo document-first | -| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | -| [DD-005](DD-005.md) | Mobile-first | -| [DD-006](DD-006.md) | AI solo se utile | -| [DD-007](DD-007.md) | Badge calcolati, non in DB | -| [DD-008](DD-008.md) | Gamification equa tra ruoli | -| [DD-009](DD-009.md) | CSI manuale, poi integrazione API | -| [DD-010](DD-010.md) | Niente storico certificati | -| [DD-011](DD-011.md) | Auth reale prima del profilo | -| [DD-012](DD-012.md) | Rimandare la migrazione ID | -| [DD-013](DD-013.md) | Portabilità dello stack | -| [DD-015](DD-015.md) | Rosa da hardcoded a DB | -| [DD-016](DD-016.md) | Schema dati Profilo Giocatore | -| [DD-017](DD-017.md) | L'admin scrive al posto del giocatore | -| [DD-018](DD-018.md) | Collegamento automatico per email | -| [DD-019](DD-019.md) | Il branch lo decide l'utente | -| [DD-020](DD-020.md) | Test obbligatori e verdi | -| [DD-021](DD-021.md) | Molle interrompibili con motion | -| [DD-022](DD-022.md) | App solo chiara | -| [DD-023](DD-023.md) | Scritture limitate per ruolo | -| [DD-024](DD-024.md) | Route di notifica autenticate | -| [DD-025](DD-025.md) | Promemoria palloni manuale | -| [DD-026](DD-026.md) | Payload push cifrato | -| [DD-027](DD-027.md) | Voto limitato ai convocati | -| [DD-028](DD-028.md) | Soglia minima Media voto e MVP | -| [DD-029](DD-029.md) | Pulizia a cascata evento cancellato | -| [DD-030](DD-030.md) | Centro notifiche in-app | -| [DD-031](DD-031.md) | Nascita pubblica sincronizzata | -| [DD-032](DD-032.md) | Stagione CSI dalle date delle gare | -| [DD-033](DD-033.md) | Gestione eventi solo da calendario | -| [DD-034](DD-034.md) | Ruolo allenatore | -| [DD-035](DD-035.md) | Avviso certificati in Home | -| [DD-036](DD-036.md) | Notifiche email via Gmail | -| [DD-037](DD-037.md) | Promemoria evento senza ritorni | -| [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 | +| ID | Titolo | +| ------------------- | ----------------------------------------- | +| [DD-001](DD-001.md) | Indipendenza da Lovable | +| [DD-002](DD-002.md) | Sviluppo document-first | +| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | +| [DD-005](DD-005.md) | Mobile-first | +| [DD-006](DD-006.md) | AI solo se utile | +| [DD-007](DD-007.md) | Badge calcolati, non in DB | +| [DD-008](DD-008.md) | Gamification equa tra ruoli | +| [DD-009](DD-009.md) | CSI manuale, poi integrazione API | +| [DD-010](DD-010.md) | Niente storico certificati | +| [DD-011](DD-011.md) | Auth reale prima del profilo | +| [DD-012](DD-012.md) | Rimandare la migrazione ID | +| [DD-013](DD-013.md) | Portabilità dello stack | +| [DD-015](DD-015.md) | Rosa da hardcoded a DB | +| [DD-016](DD-016.md) | Schema dati Profilo Giocatore | +| [DD-017](DD-017.md) | L'admin scrive al posto del giocatore | +| [DD-018](DD-018.md) | Collegamento automatico per email | +| [DD-019](DD-019.md) | Il branch lo decide l'utente | +| [DD-020](DD-020.md) | Test obbligatori e verdi | +| [DD-021](DD-021.md) | Molle interrompibili con motion | +| [DD-022](DD-022.md) | App solo chiara | +| [DD-023](DD-023.md) | Scritture limitate per ruolo | +| [DD-024](DD-024.md) | Route di notifica autenticate | +| [DD-025](DD-025.md) | Promemoria palloni manuale | +| [DD-026](DD-026.md) | Payload push cifrato | +| [DD-027](DD-027.md) | Voto limitato ai convocati | +| [DD-028](DD-028.md) | Soglia minima Media voto e MVP | +| [DD-029](DD-029.md) | Pulizia a cascata evento cancellato | +| [DD-030](DD-030.md) | Centro notifiche in-app | +| [DD-031](DD-031.md) | Nascita pubblica sincronizzata | +| [DD-032](DD-032.md) | Stagione CSI dalle date delle gare | +| [DD-033](DD-033.md) | Gestione eventi solo da calendario | +| [DD-034](DD-034.md) | Ruolo allenatore | +| [DD-035](DD-035.md) | Avviso certificati in Home | +| [DD-036](DD-036.md) | Notifiche email via Gmail | +| [DD-037](DD-037.md) | Promemoria evento senza ritorni | +| [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-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni | **In valutazione** diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index 785fc6f..0e00435 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -82,7 +82,7 @@ sulla tab Stagione. ### Avviso certificati -**Stato:** implementato (DD-035). +**Stato:** implementato (DD-035, soglia del giocatore DD-041). 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 @@ -90,13 +90,13 @@ 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) | — | +| 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 | **entrambi**: lo staff (senza il suo nome) e il proprio personale, a 30 giorni | personale: apre `/profilo?tab=documenti` | +| 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 @@ -109,15 +109,24 @@ 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`. +`giorni` è la differenza in giorni di calendario tra la scadenza e oggi (ora locale). La +soglia del giallo dipende da chi guarda (DD-041): il giocatore è avvisato un mese prima dello +staff. -| 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 | +| Chi guarda | Soglia | +| ---------- | ------------------------------------------ | +| Giocatore | `GIORNI_AVVISO_CERTIFICATO_GIOCATORE = 30` | +| Admin | `GIORNI_AVVISO_CERTIFICATO = 7` | + +| Condizione | Stato | Avviso | +| --------------------- | ----------- | ------------------ | +| `giorni > soglia` | valido | nessuno | +| `0 ≤ giorni ≤ soglia` | in scadenza | giallo (`warning`) | +| `giorni < 0` | scaduto | nero (`primary`) | +| data o file mancanti | mancante | nessuno | + +Esempio: scadenza il 5 ottobre 2026. Dal 5 settembre l'avviso giallo compare solo al +giocatore; dal 28 settembre anche agli admin; dal 6 ottobre è nero per entrambi. - 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. @@ -136,7 +145,7 @@ solo proprio a un giocatore. | 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 +Il numero di giorni scende da solo ogni giorno (30… 7, 6, 5… domani, oggi): l'avviso non conserva niente, rilegge la data a ogni apertura della Home. #### Più giocatori @@ -182,14 +191,16 @@ 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 `` | +| Pezzo | Ruolo | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GIORNI_AVVISO_CERTIFICATO` (`profili-core.ts`) | la soglia dello staff, 7 | +| `GIORNI_AVVISO_CERTIFICATO_GIOCATORE` | la soglia del giocatore, 30 (DD-041) | +| `giorniAllaScadenza(scadenza, oggi)` | differenza in giorni di calendario tra due date `AAAA-MM-GG`, negativa se scaduto | +| `avvisiCertificati(rosa, profili, oggi, soglia)` | funzione pura (`soglia` di default 7): restituisce `{ scaduti, inScadenza }`, ognuno una lista ordinata di `{ giocatoreId, nome, cognome, scadenza, giorni }` | +| `avvisiCertificatiUtente(rosa, profili, oggi, utente)` | funzione pura: `{ personale, staff }` per chi guarda; l'admin giocatore ha entrambi, senza il suo nome nello staff (DD-041) | +| `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. @@ -197,7 +208,7 @@ la Home non deve rompersi per un dato accessorio. `/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), +- Test in `test/unit/profili-core.test.ts`: avvisi per utente (admin giocatore, admin puro, giocatore semplice), soglia dello staff (8 giorni no, 7 sì, 0 sì, −1 scaduto) e del giocatore (31 no, 30 sì), 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. @@ -254,8 +265,8 @@ 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 +Quando mancano 30 giorni o meno alla scadenza, o il certificato è scaduto, il giocatore +riceve un avviso nella propria Home; compare in quello degli admin da 7 giorni prima (vedi [Avviso certificati](#avviso-certificati)). Lo storico non viene mantenuto nella prima versione. diff --git a/src/components/crapp/AvvisoCertificati.tsx b/src/components/crapp/AvvisoCertificati.tsx index 94b4ac9..d0a68ad 100644 --- a/src/components/crapp/AvvisoCertificati.tsx +++ b/src/components/crapp/AvvisoCertificati.tsx @@ -2,7 +2,7 @@ import { AlertCircle } from "lucide-react"; import { Link } from "@tanstack/react-router"; import { useProfili } from "@/lib/profili"; import { - avvisiCertificati, + avvisiCertificatiUtente, formatDataBreve, testoScadenza, type AvvisoCertificato, @@ -49,7 +49,10 @@ function Card({ ); } -/** Avviso certificati in scadenza/scaduti (DD-035): calcolato al volo, niente push. */ +/** + * Avviso certificati in scadenza/scaduti (DD-035, DD-041): calcolato al volo, niente push. + * Il giocatore lo vede da 30 giorni prima della scadenza, lo staff da 7. + */ export function AvvisoCertificati() { const admin = useIsAdmin(); const ruoliPronti = useRuoliPronti(); @@ -61,50 +64,48 @@ export function AvvisoCertificati() { 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} - - ); - } + // Admin che è anche giocatore: vede il proprio avviso a 30 giorni e quello dello staff (DD-041). + const { personale, staff } = avvisiCertificatiUtente(rosa, profili, oggi, { admin, base }); + const cardPersonale = personale ? ( + personale.giorni < 0 ? ( + + ) : ( + + ) + ) : 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 ? ( - - ) : ( - + if (!staff) return cardPersonale; + const { scaduti, inScadenza } = staff; + return ( + <> + {cardPersonale} + {scaduti.length > 0 ? ( + rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} + /> + ) : null} + {inScadenza.length > 0 ? ( + rigaStaff(a, testoScadenza(a.giorni)))} + /> + ) : null} + ); } diff --git a/src/lib/profili-core.ts b/src/lib/profili-core.ts index aff48d6..0841b58 100644 --- a/src/lib/profili-core.ts +++ b/src/lib/profili-core.ts @@ -216,9 +216,12 @@ export function etichettaGiocatore(g: GiocatoreSquadra): string { return `#${g.numero} ${nomeCompleto(g)}`; } -/** Soglia dell'avviso certificati in Home (DD-035). */ +/** Soglia dell'avviso certificati dello staff in Home (DD-035). */ export const GIORNI_AVVISO_CERTIFICATO = 7; +/** Soglia dell'avviso personale: il giocatore è avvisato un mese prima dello staff (DD-041). */ +export const GIORNI_AVVISO_CERTIFICATO_GIOCATORE = 30; + /** 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`); @@ -258,11 +261,13 @@ function confrontaAvvisi(a: AvvisoCertificato, b: AvvisoCertificato): number { * 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). + * `soglia` sono i giorni di preavviso: 7 per lo staff, 30 per il giocatore (DD-041). */ export function avvisiCertificati( rosa: GiocatoreSquadra[], profili: Record, oggi: string, + soglia: number = GIORNI_AVVISO_CERTIFICATO, ): { scaduti: AvvisoCertificato[]; inScadenza: AvvisoCertificato[] } { const scaduti: AvvisoCertificato[] = []; const inScadenza: AvvisoCertificato[] = []; @@ -271,7 +276,7 @@ export function avvisiCertificati( const p = profili[g.id]; if (!p?.certificatoScadenza || !p.certificatoPath) continue; const giorni = giorniAllaScadenza(p.certificatoScadenza, oggi); - if (giorni > GIORNI_AVVISO_CERTIFICATO) continue; + if (giorni > soglia) continue; const avviso: AvvisoCertificato = { giocatoreId: g.id, nome: g.nome, @@ -285,3 +290,32 @@ export function avvisiCertificati( inScadenza.sort(confrontaAvvisi); return { scaduti, inScadenza }; } + +/** + * Avvisi certificati per chi guarda la Home (DD-035, DD-041): `personale` è il proprio + * certificato (soglia 30 giorni), per chiunque sia un giocatore in rosa; `staff` sono gli + * altri della rosa (soglia 7 giorni), solo per l'admin, senza il suo nome perché già nel + * personale. + */ +export function avvisiCertificatiUtente( + rosa: GiocatoreSquadra[], + profili: Record, + oggi: string, + utente: { admin: boolean; base: GiocatoreSquadra | null }, +): { + personale: AvvisoCertificato | undefined; + staff: { scaduti: AvvisoCertificato[]; inScadenza: AvvisoCertificato[] } | null; +} { + const mio = utente.base + ? avvisiCertificati([utente.base], profili, oggi, GIORNI_AVVISO_CERTIFICATO_GIOCATORE) + : null; + const personale = mio ? (mio.scaduti[0] ?? mio.inScadenza[0]) : undefined; + const staff = utente.admin + ? avvisiCertificati( + rosa.filter((g) => g.id !== utente.base?.id), + profili, + oggi, + ) + : null; + return { personale, staff }; +} diff --git a/test/unit/profili-core.test.ts b/test/unit/profili-core.test.ts index aa9f2a8..7657399 100644 --- a/test/unit/profili-core.test.ts +++ b/test/unit/profili-core.test.ts @@ -3,6 +3,8 @@ import assert from "node:assert/strict"; import { aRigaProfilo, avvisiCertificati, + avvisiCertificatiUtente, + GIORNI_AVVISO_CERTIFICATO_GIOCATORE, completamento, completamentoAllenatore, csvTesseramento, @@ -333,4 +335,78 @@ assert.deepEqual( "il più vecchio prima, poi alfabetico a parità di data (Alfa prima di Zeta)", ); +// soglia del giocatore (DD-041): 30 giorni di preavviso invece di 7 +const rosaMese: 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 profiliMese: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-10-21", certificatoPath: "p1" }, // 31 giorni: fuori + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-10-20", certificatoPath: "p2" }, // 30 giorni: dentro + g3: { ...vuoto, giocatoreId: "g3", certificatoScadenza: "2026-09-10", certificatoPath: "p3" }, // scaduto +}; +assert.equal(GIORNI_AVVISO_CERTIFICATO_GIOCATORE, 30); +const mese = avvisiCertificati( + rosaMese, + profiliMese, + oggiTest, + GIORNI_AVVISO_CERTIFICATO_GIOCATORE, +); +assert.deepEqual( + mese.inScadenza.map((a) => a.giocatoreId), + ["g2"], + "31 giorni resta valido, 30 genera l'avviso del giocatore", +); +assert.deepEqual( + mese.scaduti.map((a) => a.giocatoreId), + ["g3"], +); +const staffMese = avvisiCertificati(rosaMese, profiliMese, oggiTest); +assert.equal(staffMese.inScadenza.length, 0, "senza soglia esplicita resta 7 giorni (staff)"); +assert.deepEqual( + staffMese.scaduti.map((a) => a.giocatoreId), + ["g3"], +); + +// avvisi per utente (DD-041): admin giocatore, admin puro, giocatore semplice +const rosaUtente: GiocatoreSquadra[] = [ + { ...squadra[0]!, id: "g1", nome: "Ivan", cognome: "Cacciari" }, + { ...squadra[0]!, id: "g2", nome: "Anna", cognome: "Bruni" }, +]; +const profiliUtente: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-10-10", certificatoPath: "p1" }, // 20 giorni + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-09-25", certificatoPath: "p2" }, // 5 giorni +}; +const adminGiocatore = avvisiCertificatiUtente(rosaUtente, profiliUtente, oggiTest, { + admin: true, + base: rosaUtente[0]!, +}); +assert.equal( + adminGiocatore.personale?.giocatoreId, + "g1", + "admin giocatore: avviso personale a 30 giorni", +); +assert.deepEqual( + adminGiocatore.staff?.inScadenza.map((a) => a.giocatoreId), + ["g2"], + "lo staff non ripete il suo nome", +); +const adminPuro = avvisiCertificatiUtente(rosaUtente, profiliUtente, oggiTest, { + admin: true, + base: null, +}); +assert.equal(adminPuro.personale, undefined); +assert.deepEqual( + adminPuro.staff?.inScadenza.map((a) => a.giocatoreId), + ["g2"], + "admin non giocatore: solo staff a 7 giorni (g1 a 20 giorni escluso)", +); +const soloGiocatore = avvisiCertificatiUtente(rosaUtente, profiliUtente, oggiTest, { + admin: false, + base: rosaUtente[0]!, +}); +assert.equal(soloGiocatore.personale?.giocatoreId, "g1"); +assert.equal(soloGiocatore.staff, null, "il giocatore semplice non vede lo staff"); + console.log("profili-core: ok"); From cdf8b144e2dd04a1717c39ba41564c5eea771f0a Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 1 Oct 2026 14:33:38 +0200 Subject: [PATCH 2/6] Turno palloni: un solo avviso, 3 ore prima dell'evento (DD-042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La migration m27 riduce il job automatico alla sola fascia delle 3 ore: l'avviso arriva una volta a chi deve portare i palloni e a chi li deve prendere. I tipi turno_palloni_12h e _6h restano ammessi per le notifiche già generate e per la revoca. Migration applicata in produzione. Test di integrazione aggiornati (nessun avviso a 4,5 e 9 ore); documentati DD-042, moduli, DATABASE, ROADMAP, glossario e changelog. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 6 + docs/DATABASE.md | 18 +-- docs/GLOSSARIO.md | 2 +- docs/ROADMAP.md | 2 +- docs/WORKER_EMAIL.md | 2 +- docs/decisions/DD-025.md | 4 +- docs/decisions/DD-040.md | 3 +- docs/decisions/DD-042.md | 36 ++++++ docs/decisions/README.md | 1 + docs/modules/notifiche.md | 44 +++---- docs/modules/palloni.md | 4 +- .../20261001120000_m27_palloni_solo_3h.sql | 22 ++++ test/README.md | 12 +- .../integration/notifiche-automatiche.test.ts | 109 +++++++++++------- 14 files changed, 175 insertions(+), 90 deletions(-) create mode 100644 docs/decisions/DD-042.md create mode 100644 supabase/migrations/20261001120000_m27_palloni_solo_3h.sql diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d7b7053..73746e9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,12 @@ versioni `0.y.z` erano pre-release. ### Modificato +- **Turno palloni: un solo avviso, 3 ore prima** — a chi deve portare i palloni e a chi li deve + prendere; prima ne partivano tre (12, 6 e 3 ore). Migration `m27_palloni_solo_3h`, applicata in + produzione il 01/10/2026 (DD-042). + +### Modificato + - **Avviso certificati: giocatore a 30 giorni** — il giocatore vede l'avviso giallo sul proprio certificato medico da un mese prima della scadenza (prima 7 giorni). Per gli admin non cambia nulla: 7 giorni prima in giallo, nero dopo la scadenza (DD-041). diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 416ba6b..ccace6b 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -151,15 +151,15 @@ 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. | `promemoria-eventi-24h` (ogni ora), `-3h` (15 min) | -| `genera_avvisi_palloni()` (M26), con `genera_avvisi_palloni_fascia()` | Avvisi del turno palloni a 12, 6 e 3 ore 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), con `..._fascia()` | Solleciti a 24, 12 e 6 ore 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. | `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), con `..._fascia()` | Solleciti a 24, 12 e 6 ore 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). | — | ## Badge diff --git a/docs/GLOSSARIO.md b/docs/GLOSSARIO.md index 1812865..28565bd 100644 --- a/docs/GLOSSARIO.md +++ b/docs/GLOSSARIO.md @@ -26,4 +26,4 @@ il campionato. Dove serve il dettaglio c'è il rimando al modulo. | **Worker** | Il container Docker in `mailer/` che invia email e push dei promemoria con la service role ([WORKER_EMAIL.md](WORKER_EMAIL.md)). | | **Service role** | Chiave Supabase che bypassa la RLS. Solo server e worker, mai il client ([OPERATIONS.md](OPERATIONS.md)). | | **DD-XXX** | Decisione registrata in [decisions/](decisions/README.md). | -| **M1…M26** | Numero d'ordine delle migration (`m26_notifiche_automatiche`…) in `supabase/migrations/`. | +| **M1…M27** | Numero d'ordine delle migration (`m27_palloni_solo_3h`…) in `supabase/migrations/`. | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6db1304..1f89772 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -44,7 +44,7 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). - [x] Notifiche email — ogni notifica in-app inviata anche per email agli account registrati, da un worker Docker sull'host di casa via Gmail, con interruttore «Email» in Profilo ([specifica](modules/notifiche-email.md), DD-036) -- [x] Notifiche automatiche — promemoria con «oggi/domani», turno palloni 12, 6 e 3 ore prima, sollecito +- [x] Notifiche automatiche — promemoria con «oggi/domani», turno palloni 3 ore prima, sollecito presenze a 24, 12 e 6 ore (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040) ## Prossimo diff --git a/docs/WORKER_EMAIL.md b/docs/WORKER_EMAIL.md index 57e03b2..f24fc90 100644 --- a/docs/WORKER_EMAIL.md +++ b/docs/WORKER_EMAIL.md @@ -1,7 +1,7 @@ # Worker email: installazione e gestione Come si mette in funzione, si controlla e si diagnostica il worker (`mailer/`) che invia le notifiche -per email e le push delle notifiche che nascono nel database: promemoria a 24 e 3 ore, turno palloni a 12, 6 e 3 ore con revoca, solleciti presenze a 24, 12 e 6 ore (DD-040). Cosa fa il canale email sta in +per email e le push delle notifiche che nascono nel database: promemoria a 24 e 3 ore, turno palloni a 3 ore con revoca, solleciti presenze a 24, 12 e 6 ore (DD-040). Cosa fa il canale email sta in [modules/notifiche-email.md](modules/notifiche-email.md), la push dei promemoria in [modules/notifiche.md](modules/notifiche.md#push-dei-promemoria-m24); perché è fatto così in [DD-036](decisions/DD-036.md) diff --git a/docs/decisions/DD-025.md b/docs/decisions/DD-025.md index 061a06b..67ac8f4 100644 --- a/docs/decisions/DD-025.md +++ b/docs/decisions/DD-025.md @@ -50,5 +50,5 @@ torna utile, e con lui il segreto: DD-024 descrive già come farlo. --- -**Aggiornamento (30 settembre 2026)** — DD-040 affianca al pulsante un invio automatico a 12, 6 e 3 ore -prima dell'evento, per gli eventi con un incaricato confermato; il pulsante e la route restano. +**Aggiornamento (30 settembre 2026)** — DD-040 affianca al pulsante un invio automatico (poi ridotto a uno solo, 3 ore +prima, da DD-042), per gli eventi con un incaricato confermato; il pulsante e la route restano. diff --git a/docs/decisions/DD-040.md b/docs/decisions/DD-040.md index 7feb8f3..748f87f 100644 --- a/docs/decisions/DD-040.md +++ b/docs/decisions/DD-040.md @@ -1,8 +1,7 @@ # DD-040 — Promemoria con «oggi» o «domani», turno palloni e solleciti automatici **Data:** 30 settembre 2026 -**Stato:** Accettata (implementata e verificata in locale: migration `m26`, job, route e test; la -migration non è ancora applicata in produzione) +**Stato:** Accettata (implementata: migration `m26`, applicata in produzione il 30 settembre 2026, job, route e test). Il turno palloni è poi passato a un solo avviso a 3 ore da [DD-042](DD-042.md). **Contesto** Quattro notifiche su sei partivano da un pulsante premuto da un admin (DD-025 per i palloni): se nessuno diff --git a/docs/decisions/DD-042.md b/docs/decisions/DD-042.md new file mode 100644 index 0000000..3da08cc --- /dev/null +++ b/docs/decisions/DD-042.md @@ -0,0 +1,36 @@ +# DD-042 — Turno palloni: un solo avviso, 3 ore prima + +**Data:** 1 ottobre 2026 +**Stato:** Accettata (migration `m27`, applicata in produzione il 1 ottobre 2026; modifica la parte «turno palloni» di [DD-040](DD-040.md)) + +**Contesto** +DD-040 manda l'avviso del turno palloni a 12, 6 e 3 ore prima dell'evento: tre messaggi identici per +chi deve portare i palloni e per chi deve prenderli. Sono troppi per un'informazione che non cambia. + +**Decisione** + +1. L'avviso automatico parte **una sola volta, 3 ore prima** dell'inizio (finestra «meno di 3 ore»), a + chi deve portare i palloni («incarico assegnato») e a chi li deve prendere («riconsegna», se è + un'altra persona). Testi, destinatari, push senza email e condizione dell'incaricato confermato + restano quelli di DD-040. +2. La revoca resta: chi è stato avvisato e poi non è più destinatario riceve una sola volta «incarico + revocato». Un evento creato o assegnato a meno di 3 ore dall'inizio riceve l'avviso al giro + successivo del job. +3. **Migration `m27`:** `genera_avvisi_palloni()` chiama solo la fascia `turno_palloni_3h`. I tipi + `turno_palloni_12h` e `_6h` **restano nei `CHECK`** e nella revoca, perché notifiche e righe di + registro già generate li usano; non ne nascono di nuovi. +4. Il pulsante «Avvisa chi è di turno» non cambia. I solleciti presenze (24, 12 e 6 ore) non cambiano. + +**Alternative scartate** + +- Togliere i tipi `12h` e `6h` dai `CHECK` → le righe già generate in produzione non sarebbero più + valide e la revoca non vedrebbe più chi ha ricevuto quegli avvisi. +- Tenere anche un avviso a 12 ore → scartata su indicazione esplicita: uno solo. + +**Conseguenze** + +- Chi non apre l'app nelle 3 ore prima può non vedere l'avviso; la push è il canale principale. +- Per le notifiche palloni già generate con DD-040 non cambia nulla. + +**Riesame** +Se 3 ore risultano poche per procurarsi i palloni, alzare la finestra (una sola, mai più avvisi). diff --git a/docs/decisions/README.md b/docs/decisions/README.md index de708db..dcf0aae 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -86,6 +86,7 @@ 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-042](DD-042.md) | Turno palloni: un solo avviso a 3 ore | | [DD-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni | **In valutazione** diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md index 93bde69..1b24638 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -34,15 +34,15 @@ 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** — «Promemoria evento di _domani_ (o _oggi_): _titolo_», con data, ora e luogo | Job `pg_cron` `promemoria-eventi-24h`, ogni ora, per gli eventi che iniziano entro le prossime 24 ore. Tipo `evento_promemoria_24h`. La push la manda il worker (M24). | I convocati dell'evento, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori (DD-039). | sì (worker) | sì | sì | -| 2 | **Promemoria a 3 ore** — stesso testo | Job `pg_cron` `promemoria-eventi-3h`, ogni 15 minuti, per gli eventi che iniziano entro le prossime 3 ore. Tipo `evento_promemoria_3h`. La push la manda il worker (M24). | Come il promemoria a 24 ore. | sì (worker) | sì | sì | -| 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**, **12, 6 e 3 ore prima** dell'evento (DD-040): job `pg_cron` ogni 15 minuti, solo per allenamenti e partite con un incaricato confermato; tipi `turno_palloni_12h`, `_6h` e `_3h`, 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**, in tre tempi (DD-040): **24, 12 e 6 ore** prima dell'evento, dallo stesso job; tipi `sollecita_presenze_24h`, `_12h`, `_6h`, 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** — «Promemoria evento di _domani_ (o _oggi_): _titolo_», con data, ora e luogo | Job `pg_cron` `promemoria-eventi-24h`, ogni ora, per gli eventi che iniziano entro le prossime 24 ore. Tipo `evento_promemoria_24h`. La push la manda il worker (M24). | I convocati dell'evento, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori (DD-039). | sì (worker) | sì | sì | +| 2 | **Promemoria a 3 ore** — stesso testo | Job `pg_cron` `promemoria-eventi-3h`, ogni 15 minuti, per gli eventi che iniziano entro le prossime 3 ore. Tipo `evento_promemoria_3h`. La push la manda il worker (M24). | Come il promemoria a 24 ore. | sì (worker) | sì | sì | +| 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**, in tre tempi (DD-040): **24, 12 e 6 ore** prima dell'evento, dallo stesso job; tipi `sollecita_presenze_24h`, `_12h`, `_6h`, 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 | 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 differenza sta in come parte la push: quelle dei pulsanti (3 e 6) la mandano le route dell'app, quelle @@ -51,8 +51,8 @@ promemoria»). Il sondaggio riscrive la notifica esistente se l'admin preme di n lo stesso evento (vedi «Centro notifiche in-app»). > **Righe 1, 2, 4 e 5 (DD-040).** Implementate con la migration `m26` (job `pg_cron`, testi, revoca) e -> verificate in locale; la migration va ancora applicata in produzione. Finché non lo è, l'app si comporta -> come in 1.2.0: promemoria con il testo breve, turno palloni e sollecito presenze solo da pulsante. +> applicate in produzione (`m26` il 30/09/2026; `m27` il 01/10/2026, che porta il turno palloni a un solo +> avviso 3 ore prima, DD-042). Cosa riceve l'allenatore, notifica per notifica: [allenatore.md](allenatore.md#notifiche). @@ -146,16 +146,16 @@ Sono l'effetto attuale della logica, non scelte documentate altrove: ### Test che verificano il catalogo -| Cosa | Test | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Promemoria 24 e 3 ore: destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts` | -| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | -| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | -| 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 12·6·3 ore e revoca, solleciti a 24·12·6 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`) | `integration/notifiche-automatiche.test.ts` (nuovo), `promemoria-eventi.test.ts` (testo oggi/domani), `destinatari-notifiche.test.ts` (pulsanti), `unit/palloni-core.test.ts`, `unit/presenze.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`) | +| Cosa | Test | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promemoria 24 e 3 ore: destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts` | +| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | +| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | +| 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·12·6 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`; palloni a 3 ore: DD-042, `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` | +| 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`) | Le push dei test vanno a un servizio push finto in locale: si verifica **a chi** arrivano, non la consegna su un telefono. Non ha un test l'esclusione dell'allenatore dalle **notifiche smart**: la @@ -282,7 +282,7 @@ non richiede che il dispositivo abbia attivato «Notifiche», ha uno storico per stessa cosa delle notifiche smart (quelle restano locali, non salvate a database). Cinque sorgenti scrivono in `notifiche_utente`, mai il client (con DD-040 le sorgenti 3 e 4 hanno in più un -job `pg_cron` come la 2, che scrive `turno_palloni_12h`/`_6h`/`_3h`/`_revocato` e `sollecita_presenze_24h`/ +job `pg_cron` come la 2, che scrive `turno_palloni_3h`/`_revocato` (`_12h`/`_6h` solo fino a `m26`) e `sollecita_presenze_24h`/ `_12h`/`_6h`; i pulsanti restano e scrivono `turno_palloni` e `sollecita_presenze`): 1. **Messaggio admin** — `notifica-personalizzata.ts` inserisce una riga per destinatario diff --git a/docs/modules/palloni.md b/docs/modules/palloni.md index c7358a0..0aa6f3e 100644 --- a/docs/modules/palloni.md +++ b/docs/modules/palloni.md @@ -11,7 +11,7 @@ Gestire un turno a rotazione condiviso per chi porta e riporta i palloni ad allenamenti e partite, con proposta automatica, possibilità di modifica manuale e avvisi push, nel centro -notifiche e senza email a 12, 6 e 3 ore dall'evento (DD-040). Gli eventi extra-campo (`tipo === "evento"`, es. cena di squadra) e i +notifiche e senza email 3 ore prima dell'evento (DD-040, DD-042). Gli eventi extra-campo (`tipo === "evento"`, es. cena di squadra) e i compleanni non c'entrano: non richiedono palloni. --- @@ -56,7 +56,7 @@ oggi`, stesso criterio delle presenze): un turno assegnato in anticipo per un al ## Route API pubblica `/api/public/promemoria-palloni` > **Affiancata da un invio automatico (DD-040, migration `m26`).** Oltre al pulsante, un job -> `pg_cron` ogni 15 minuti manda l'avviso **12, 6 e 3 ore prima** dell'evento, solo se per l'evento c'è un +> `pg_cron` ogni 15 minuti manda l'avviso **una sola volta, 3 ore prima** dell'evento (DD-042, migration `m27`), solo se per l'evento c'è un > incaricato confermato in `turni_palloni`. Il pulsante e la route **restano**, con tipo > `turno_palloni` e push dalla route. I due destinatari e i testi nuovi («incarico assegnato» / > «riconsegna», validi per il pulsante e per i job, più la revoca se l'incaricato cambia) sono nel diff --git a/supabase/migrations/20261001120000_m27_palloni_solo_3h.sql b/supabase/migrations/20261001120000_m27_palloni_solo_3h.sql new file mode 100644 index 0000000..716b0a8 --- /dev/null +++ b/supabase/migrations/20261001120000_m27_palloni_solo_3h.sql @@ -0,0 +1,22 @@ +-- M27 — Turno palloni: un solo avviso, 3 ore prima dell'evento (DD-042) +-- +-- Con M26 il job mandava l'avviso a 12, 6 e 3 ore. Ne basta uno, 3 ore prima, a chi deve portare +-- i palloni e a chi li deve prendere. Cambia solo il job: i tipi `turno_palloni_12h` e `_6h` +-- restano ammessi dai CHECK perché esistono notifiche e righe di registro già generate (anche la +-- revoca le guarda), ma non ne nascono di nuove. Le funzioni `genera_avvisi_palloni_fascia` e +-- `genera_revoche_palloni` non cambiano. + +CREATE OR REPLACE FUNCTION public.genera_avvisi_palloni() +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + PERFORM public.genera_avvisi_palloni_fascia('turno_palloni_3h', interval '0', interval '3 hours'); + PERFORM public.genera_revoche_palloni(); +END; +$$; + +REVOKE ALL ON FUNCTION public.genera_avvisi_palloni() FROM PUBLIC, anon, authenticated; +GRANT EXECUTE ON FUNCTION public.genera_avvisi_palloni() TO service_role; diff --git a/test/README.md b/test/README.md index 2979f9d..5b7a5e2 100644 --- a/test/README.md +++ b/test/README.md @@ -24,12 +24,12 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push ## Struttura -| Cartella | Cosa verifica | Serve rete? | -| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No | -| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26, DD-040) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì | -| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì | -| `helpers/` | Avvio del server di test e mini-harness condiviso | — | +| Cartella | Cosa verifica | Serve rete? | +| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No | +| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26 e M27, DD-040 e DD-042: palloni un solo avviso a 3 ore) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì | +| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì | +| `helpers/` | Avvio del server di test e mini-harness condiviso | — | ## Server di sviluppo con Node vecchio diff --git a/test/integration/notifiche-automatiche.test.ts b/test/integration/notifiche-automatiche.test.ts index c7b2245..68e2ab3 100644 --- a/test/integration/notifiche-automatiche.test.ts +++ b/test/integration/notifiche-automatiche.test.ts @@ -5,7 +5,8 @@ * Esegue i job veri del database (`genera_avvisi_palloni`, `genera_solleciti_presenze`) su eventi * creati con orari relativi a «adesso» e verifica: * - * - le finestre contigue e senza sovrapposizione (palloni 12·6·3 ore, solleciti 24·12·6 ore); + * - le finestre (palloni: un solo avviso nelle 3 ore prima; solleciti 24·12·6 ore, contigue e senza + * sovrapposizione); * - i destinatari (incaricato e turno precedente; convocati senza risposta o con «forse»; mai * l'allenatore né chi non è più attivo); * - una sola generazione per giocatore, evento e tipo, anche se la notifica viene eliminata; @@ -46,12 +47,14 @@ if (!locale) { const INATTIVO = "g9"; const ALLENATORE = "g10"; - // Palloni: in ordine cronologico PASSATO < PREC < MAIN < EV6 < EV12 < LONTANO. + // Palloni: in ordine cronologico PASSATO < PREC < MAIN < SECONDO < TERZO < FUORI4 < FUORI9 < LONTANO. const PASSATO = `${PREFISSO}-passato`; const PREC = `${PREFISSO}-prec`; const MAIN = `${PREFISSO}-main`; - const EV6 = `${PREFISSO}-ev6`; - const EV12 = `${PREFISSO}-ev12`; + const SECONDO = `${PREFISSO}-secondo`; + const TERZO = `${PREFISSO}-terzo`; + const FUORI4 = `${PREFISSO}-fuori4`; + const FUORI9 = `${PREFISSO}-fuori9`; const LONTANO = `${PREFISSO}-lontano`; const GENERICO = `${PREFISSO}-generico`; const COPPIA_A = `${PREFISSO}-coppia-a`; @@ -68,8 +71,10 @@ if (!locale) { PASSATO, PREC, MAIN, - EV6, - EV12, + SECONDO, + TERZO, + FUORI4, + FUORI9, LONTANO, GENERICO, COPPIA_A, @@ -266,36 +271,51 @@ if (!locale) { ); await scrivi(`giocatori_squadra?id=eq.${INATTIVO}`, "PATCH", { attivo: false }, tokenAdmin); - // Palloni. Le ore scelte stanno a più di mezz'ora dai confini delle finestre. + // Palloni (DD-042): un solo avviso, nelle 3 ore prima. MAIN, SECONDO e TERZO sono dentro la + // finestra; FUORI4 e FUORI9 hanno un incaricato ma sono ancora fuori (tra 4,5 e 9 ore). + // Le ore scelte stanno a più di mezz'ora dai confini della finestra. await creaEvento(PASSATO, "partita", tra(-60)); await creaEvento(PREC, "allenamento", tra(-30)); - await creaEvento(MAIN, "allenamento", tra(2.5)); - await creaEvento(EV6, "partita", tra(4.5)); - await creaEvento(EV12, "allenamento", tra(9)); + await creaEvento(MAIN, "allenamento", tra(0.8)); + await creaEvento(SECONDO, "partita", tra(1.5)); + await creaEvento(TERZO, "allenamento", tra(2.2)); + await creaEvento(FUORI4, "allenamento", tra(4.5)); + await creaEvento(FUORI9, "allenamento", tra(9)); await creaEvento(LONTANO, "allenamento", tra(40)); - await creaEvento(GENERICO, "evento", tra(2.2)); + await creaEvento(GENERICO, "evento", tra(1.9)); await assegna(PREC, G5); await assegna(MAIN, G4); - await assegna(EV6, G7); - await assegna(EV12, G8); + await assegna(SECONDO, G7); + await assegna(TERZO, G8); + await assegna(FUORI4, G7); + await assegna(FUORI9, G8); await assegna(GENERICO, G4); // extra-campo: il turno non conta // --- Turno palloni: finestre e destinatari --------------------------------------------- - await prova("ogni evento riceve l'avviso della sua fascia e di nessun'altra", async () => { - await esegui("genera_avvisi_palloni"); - assert.deepEqual(await destinatari(MAIN, "turno_palloni_3h"), [G4, G5]); - assert.deepEqual(await destinatari(EV6, "turno_palloni_6h"), [G4, G7]); - assert.deepEqual(await destinatari(EV12, "turno_palloni_12h"), [G7, G8]); - for (const [evento, altri] of [ - [MAIN, ["turno_palloni_6h", "turno_palloni_12h"]], - [EV6, ["turno_palloni_3h", "turno_palloni_12h"]], - [EV12, ["turno_palloni_3h", "turno_palloni_6h"]], - ] as const) { - for (const tipo of altri) { - assert.equal((await notifiche(evento, tipo)).length, 0, `${evento} non ha ${tipo}`); + await prova( + "un solo avviso, 3 ore prima, a chi porta i palloni e a chi li prende", + async () => { + await esegui("genera_avvisi_palloni"); + assert.deepEqual(await destinatari(MAIN, "turno_palloni_3h"), [G4, G5]); + assert.deepEqual(await destinatari(SECONDO, "turno_palloni_3h"), [G4, G7]); + assert.deepEqual(await destinatari(TERZO, "turno_palloni_3h"), [G7, G8]); + for (const evento of [MAIN, SECONDO, TERZO]) { + for (const tipo of ["turno_palloni_12h", "turno_palloni_6h"]) { + assert.equal((await notifiche(evento, tipo)).length, 0, `${evento} non ha ${tipo}`); + } } - } - }); + }, + ); + + await prova( + "a 4,5 e 9 ore dall'inizio non parte nessun avviso, anche con l'incaricato", + async () => { + assert.equal((await notifiche(FUORI4)).length, 0, "tra 4,5 ore: prima era la fascia 6 ore"); + assert.equal((await notifiche(FUORI9)).length, 0, "tra 9 ore: prima era la fascia 12 ore"); + assert.equal((await registro(FUORI4)).length, 0); + assert.equal((await registro(FUORI9)).length, 0); + }, + ); await prova( "nessun avviso per eventi lontani, passati, extra-campo o senza incaricato", @@ -314,8 +334,8 @@ if (!locale) { const turni = await turniDelDatabase(); for (const [evento, tipo] of [ [MAIN, "turno_palloni_3h"], - [EV6, "turno_palloni_6h"], - [EV12, "turno_palloni_12h"], + [SECONDO, "turno_palloni_3h"], + [TERZO, "turno_palloni_3h"], ] as const) { const attesi = avvisiPalloniEvento(turni, eventi, evento); assert.equal(attesi.length, 2, evento); @@ -330,7 +350,7 @@ if (!locale) { const [incarico] = (await notifiche(MAIN, "turno_palloni_3h")).filter( (r) => r.giocatore_id === G4, ); - const eventiEv6 = eventi.find((e) => e.id === EV6)!; + const eventoSecondo = eventi.find((e) => e.id === SECONDO)!; const main = eventi.find((e) => e.id === MAIN)!; assert.equal( incarico?.corpo, @@ -338,7 +358,7 @@ if (!locale) { `Evento: ${PREFISSO} main`, `Data: ${gg(main.data)}, ore ${main.ora}`, "Incarico: custodia dei palloni al termine dell'evento", - `Riconsegna: ${gg(eventiEv6.data)}`, + `Riconsegna: ${gg(eventoSecondo.data)}`, ].join("\n"), "la riconsegna è la data dell'evento successivo", ); @@ -349,8 +369,8 @@ if (!locale) { assert.equal((await registro(MAIN, "turno_palloni_3h")).length, 2); const righe = [ ...(await notifiche(MAIN, "turno_palloni_3h")), - ...(await notifiche(EV6, "turno_palloni_6h")), - ...(await notifiche(EV12, "turno_palloni_12h")), + ...(await notifiche(SECONDO, "turno_palloni_3h")), + ...(await notifiche(TERZO, "turno_palloni_3h")), ]; const ids = righe.map((r) => r.id); assert.equal(ids.length, 6); @@ -385,13 +405,13 @@ if (!locale) { "ora non valida: escluso, senza errori", ); - await scrivi(`turni_palloni?evento_id=eq.${EV12}`, "PATCH", { giocatore_id: INATTIVO }); + await scrivi(`turni_palloni?evento_id=eq.${TERZO}`, "PATCH", { giocatore_id: INATTIVO }); await esegui("genera_avvisi_palloni"); assert.ok( - !(await destinatari(EV12, "turno_palloni_12h")).includes(INATTIVO), + !(await destinatari(TERZO, "turno_palloni_3h")).includes(INATTIVO), "chi non è più attivo non riceve nulla", ); - await cambiaTurno(EV12, G8); + await cambiaTurno(TERZO, G8); }, ); @@ -452,18 +472,19 @@ if (!locale) { "turno tolto: chi era stato avvisato, incaricato o riconsegna, viene revocato", async () => { // La notifica dell'avviso può essere stata eliminata: conta il registro. - const [avvisoG7] = (await notifiche(EV6, "turno_palloni_6h")).filter( + const [avvisoG7] = (await notifiche(SECONDO, "turno_palloni_3h")).filter( (r) => r.giocatore_id === G7, ); await scrivi(`notifiche_utente?id=eq.${avvisoG7!.id}`, "DELETE"); - await scrivi(`turni_palloni?evento_id=eq.${EV6}`, "DELETE"); + await scrivi(`turni_palloni?evento_id=eq.${SECONDO}`, "DELETE"); await esegui("genera_avvisi_palloni"); - const revocati = await destinatari(EV6, "turno_palloni_revocato"); + const revocati = await destinatari(SECONDO, "turno_palloni_revocato"); assert.ok(revocati.includes(G7), "g7 aveva eliminato l'avviso: conta il registro"); assert.ok(!revocati.includes(G8), "g8 non c'entra con questo evento"); - // Senza incaricato non parte nulla: nemmeno un nuovo avviso di fascia per nessuno. - assert.equal((await notifiche(EV6, "turno_palloni_3h")).length, 0); - await assegna(EV6, G7); + // Senza incaricato non parte nulla di nuovo: restano g4 e g6, avvisati prima come + // «riconsegna» quando il turno di MAIN è passato a g6 e poi tornato a g4. + assert.deepEqual(await destinatari(SECONDO, "turno_palloni_3h"), [G4, G6]); + await assegna(SECONDO, G7); }, ); @@ -511,12 +532,12 @@ if (!locale) { // Solo i due eventi della coppia cadono nella fascia: 8 giorni ± un giorno. await esegui("genera_avvisi_palloni_fascia", { - p_tipo: "turno_palloni_12h", + p_tipo: "turno_palloni_3h", p_da: `${24 * 7} hours`, p_a: `${24 * 9} hours`, }); for (const evento of [COPPIA_A, COPPIA_B]) { - const righe = await notifiche(evento, "turno_palloni_12h"); + const righe = await notifiche(evento, "turno_palloni_3h"); const attesi = avvisiPalloniEvento(turni, eventi, evento); assert.equal(righe.length, attesi.length, evento); for (const a of attesi) { From e822e4574b1f61baa0c0e1e1065bbfebafad42cd Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 1 Oct 2026 14:49:30 +0200 Subject: [PATCH 3/6] Rimuove il promemoria evento a 24 e 3 ore (DD-043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La migration m28 toglie i due job pg_cron che generavano il promemoria prima di ogni evento. Funzione, tipi, registro e notifiche già generate restano. Migration applicata in produzione. Nuovo test sui job non più pianificati; documentati DD-043, modulo notifiche, DATABASE, ROADMAP, WORKER_EMAIL e changelog. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 6 ++ docs/DATABASE.md | 2 +- docs/EFFICIENZA_CLOUD.md | 2 +- docs/ROADMAP.md | 4 +- docs/WORKER_EMAIL.md | 2 +- docs/decisions/DD-043.md | 35 +++++++++++ docs/decisions/README.md | 1 + docs/modules/allenatore.md | 16 ++--- docs/modules/notifiche-email.md | 3 +- docs/modules/notifiche.md | 58 ++++++++++--------- src/lib/notifiche-utente.ts | 2 +- src/routes/calendario.tsx | 2 +- ...001150000_m28_niente_promemoria_evento.sql | 14 +++++ .../integration/notifiche-automatiche.test.ts | 28 +++++++++ 14 files changed, 130 insertions(+), 45 deletions(-) create mode 100644 docs/decisions/DD-043.md create mode 100644 supabase/migrations/20261001150000_m28_niente_promemoria_evento.sql diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 73746e9..bb9cba9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,12 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +### Rimosso + +- **Promemoria evento a 24 e 3 ore** — non partono più per nessun evento (allenamenti, partite, extra-campo): + niente notifica, push né email. Migration `m28_niente_promemoria_evento` (tolti i due job `pg_cron`; + funzione, tipi e notifiche già generate restano). Applicata in produzione il 01/10/2026 (DD-043). + ### Modificato - **Turno palloni: un solo avviso, 3 ore prima** — a chi deve portare i palloni e a chi li deve diff --git a/docs/DATABASE.md b/docs/DATABASE.md index ccace6b..0aa1b87 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -153,7 +153,7 @@ riservate alla service role (anon e utenti autenticati non possono chiamarle). T | 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. | `promemoria-eventi-24h` (ogni ora), `-3h` (15 min) | +| `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), con `..._fascia()` | Solleciti a 24, 12 e 6 ore ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) | diff --git a/docs/EFFICIENZA_CLOUD.md b/docs/EFFICIENZA_CLOUD.md index d782dc6..3a93f4b 100644 --- a/docs/EFFICIENZA_CLOUD.md +++ b/docs/EFFICIENZA_CLOUD.md @@ -19,7 +19,7 @@ Query, traffico e invocazioni vanno tenuti al minimo **per costruzione**, non ot si ricalcolano a ogni apertura di pagina. I badge restano calcolati a runtime dai dati già in cache, senza query aggiuntive (DD-007): `src/lib/rosa.ts` aggrega ciò che è già stato letto. -6. **Push solo per eventi importanti**: convocazioni, promemoria allenamento/partita, turno +6. **Push solo per eventi importanti**: convocazioni, turno palloni, esito finale. 7. **Niente funzionalità pesanti**: foto, video, chat. 8. **Indici** sui campi usati per filtri e relazioni in ogni nuova migration. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1f89772..1afcd3e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -44,8 +44,8 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). - [x] Notifiche email — ogni notifica in-app inviata anche per email agli account registrati, da un worker Docker sull'host di casa via Gmail, con interruttore «Email» in Profilo ([specifica](modules/notifiche-email.md), DD-036) -- [x] Notifiche automatiche — promemoria con «oggi/domani», turno palloni 3 ore prima, sollecito - presenze a 24, 12 e 6 ore (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040) +- [x] Notifiche automatiche — turno palloni 3 ore prima, sollecito + presenze a 24, 12 e 6 ore (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) ## Prossimo diff --git a/docs/WORKER_EMAIL.md b/docs/WORKER_EMAIL.md index f24fc90..17adc4d 100644 --- a/docs/WORKER_EMAIL.md +++ b/docs/WORKER_EMAIL.md @@ -1,7 +1,7 @@ # Worker email: installazione e gestione Come si mette in funzione, si controlla e si diagnostica il worker (`mailer/`) che invia le notifiche -per email e le push delle notifiche che nascono nel database: promemoria a 24 e 3 ore, turno palloni a 3 ore con revoca, solleciti presenze a 24, 12 e 6 ore (DD-040). Cosa fa il canale email sta in +per email e le push delle notifiche che nascono nel database: turno palloni a 3 ore con revoca, solleciti presenze a 24, 12 e 6 ore (DD-040). Cosa fa il canale email sta in [modules/notifiche-email.md](modules/notifiche-email.md), la push dei promemoria in [modules/notifiche.md](modules/notifiche.md#push-dei-promemoria-m24); perché è fatto così in [DD-036](decisions/DD-036.md) diff --git a/docs/decisions/DD-043.md b/docs/decisions/DD-043.md new file mode 100644 index 0000000..026c21d --- /dev/null +++ b/docs/decisions/DD-043.md @@ -0,0 +1,35 @@ +# DD-043 — Niente promemoria automatico a 24 e 3 ore prima degli eventi + +**Data:** 1 ottobre 2026 +**Stato:** Accettata (migration `m28`, applicata in produzione il 1 ottobre 2026; sostituisce il promemoria di +[DD-030](DD-030.md), [DD-037](DD-037.md) e [DD-040](DD-040.md)) + +**Contesto** +Il promemoria a 24 e 3 ore prima di ogni evento non serve: la squadra ha già il calendario, il +sollecito presenze (per chi non ha risposto) e l'avviso del turno palloni a 3 ore. + +**Decisione** + +1. Il database **non genera più** `evento_promemoria_24h` ed `evento_promemoria_3h`, per nessun tipo di + evento (allenamento, partita, extra-campo). +2. La migration `m28` toglie solo i due job `pg_cron` (`promemoria-eventi-24h` e `-3h`). + `genera_promemoria_eventi()`, i tipi nei `CHECK`, il registro `promemoria_eventi_generati` e la coda + push restano: le notifiche già create continuano a esistere e rimettere il promemoria richiede solo + un `cron.schedule`. +3. Non cambiano: sollecito presenze, turno palloni (DD-042), messaggio dello staff, sondaggio. + +**Alternative scartate** + +- Togliere il promemoria solo per gli allenamenti → scartata su indicazione esplicita: via per tutti. +- Cancellare funzione, tipi e registro → scartata: vanno tenute le notifiche già generate e il lavoro per + rimetterlo sarebbe molto più grande di un `cron.schedule`. + +**Conseguenze** + +- Meno notifiche per la squadra. Chi voleva il promemoria di una partita non lo riceve più. +- Funzione, test e registro restano come codice non pianificato: sono ancora coperti da + `promemoria-eventi.test.ts` e dagli altri test sulla funzione. + +**Riesame** +Se la squadra sente la mancanza del promemoria (per esempio solo per le partite), rimettere i job o +reintrodurlo per tipo di evento. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index dcf0aae..09bb665 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -86,6 +86,7 @@ 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-043](DD-043.md) | Niente promemoria evento a 24 e 3 ore | | [DD-042](DD-042.md) | Turno palloni: un solo avviso a 3 ore | | [DD-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni | diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index 6f40313..91ea452 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -125,14 +125,14 @@ risposte presenze altrui, gestione dei ruoli. Cosa riceve l'allenatore, notifica per notifica (il catalogo completo, con i canali di tutti, sta in [notifiche.md](notifiche.md#catalogo-delle-notifiche)): -| Notifica | L'allenatore la riceve? | Note | -| ------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Promemoria a 24 e 3 ore prima di un evento | **no** | Dal 29/09/2026 (m25, DD-039) `giocatori_destinatari_evento()` include solo i giocatori attivi: l'allenatore non può rispondere alle presenze né essere convocato, quindi il promemoria non gli chiederebbe nulla che possa fare (per esempio una cena di squadra). Prima gli arrivava sempre, convocato o no (m21). | -| Messaggio dello staff | **sì** | In-app ed email vanno a tutta la rosa attiva, allenatori compresi; la push ai suoi dispositivi iscritti, o solo a lui se il messaggio è per lui. | -| 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. | -| 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. | +| Notifica | L'allenatore la riceve? | Note | +| ------------------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promemoria a 24 e 3 ore prima di un evento (rimosso, DD-043) | **no** | Dal 29/09/2026 (m25, DD-039) `giocatori_destinatari_evento()` include solo i giocatori attivi: l'allenatore non può rispondere alle presenze né essere convocato, quindi il promemoria non gli chiederebbe nulla che possa fare (per esempio una cena di squadra). Prima gli arrivava sempre, convocato o no (m21). | +| Messaggio dello staff | **sì** | In-app ed email vanno a tutta la rosa attiva, allenatori compresi; la push ai suoi dispositivi iscritti, o solo a lui se il messaggio è per lui. | +| 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. | +| 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 spento l'interruttore «Email»; la push solo sui dispositivi su cui ha attivato «Notifiche» dal diff --git a/docs/modules/notifiche-email.md b/docs/modules/notifiche-email.md index dedd0d8..a2b2662 100644 --- a/docs/modules/notifiche-email.md +++ b/docs/modules/notifiche-email.md @@ -49,8 +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, promemoria a 24 ore e a -3 ore, 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. **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 1b24638..83139c8 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -36,8 +36,8 @@ 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** — «Promemoria evento di _domani_ (o _oggi_): _titolo_», con data, ora e luogo | Job `pg_cron` `promemoria-eventi-24h`, ogni ora, per gli eventi che iniziano entro le prossime 24 ore. Tipo `evento_promemoria_24h`. La push la manda il worker (M24). | I convocati dell'evento, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori (DD-039). | sì (worker) | sì | sì | -| 2 | **Promemoria a 3 ore** — stesso testo | Job `pg_cron` `promemoria-eventi-3h`, ogni 15 minuti, per gli eventi che iniziano entro le prossime 3 ore. Tipo `evento_promemoria_3h`. La push la manda il worker (M24). | Come il promemoria a 24 ore. | sì (worker) | sì | sì | +| 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**, in tre tempi (DD-040): **24, 12 e 6 ore** prima dell'evento, dallo stesso job; tipi `sollecita_presenze_24h`, `_12h`, `_6h`, 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ì | @@ -46,7 +46,7 @@ quelle del turno palloni**, che restano solo push e in-app: DD-040). 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 differenza sta in come parte la push: quelle dei pulsanti (3 e 6) la mandano le route dell'app, quelle -automatiche (1, 2, 4 e 5) nascono nel database e la manda il worker `mailer/` (vedi «Push dei +automatiche (4 e 5; 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,17 +63,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 | `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.` | -Le righe 1-2, 4a-4c e 5 nascono nelle funzioni SQL della migration `m26` +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`, `genera_solleciti_presenze_fascia`); i pulsanti usano gli stessi testi da `palloni-core.ts` (`avvisiPalloniEvento`) e `presenze.ts` (`testoSollecito`), e `notifiche-automatiche.test.ts` verifica che siano @@ -96,8 +96,8 @@ dall'emoji. Non entrano nel centro notifiche e non generano email. ### Cosa non genera nessuna notifica -- Creare, spostare, modificare o cancellare un evento: contano solo i promemoria prima - dell'inizio (DD-030). +- Creare, spostare, modificare o cancellare un evento. Dalla `m28` (DD-043) non parte nemmeno più un + promemoria prima dell'inizio: restano solo il turno palloni e il sollecito presenze. - L'arrivo di un risultato, di una pagella o di un voto MVP. - Un giocatore che risponde a un evento: nessun avviso a chi gestisce gli eventi. - Assenze e infortuni. @@ -106,7 +106,9 @@ dall'emoji. Non entrano nel centro notifiche e non generano email. ### Comportamenti da conoscere -Sono l'effetto attuale della logica, non scelte documentate altrove: +Sono l'effetto attuale della logica, non scelte documentate altrove. I punti sui **promemoria 1 e 2** +descrivono come funzionavano fino alla `m28`: la funzione esiste ancora ma nessun job la chiama +(DD-043); valgono solo se i job vengono ripianificati. - **I promemoria (1 e 2) non distinguono il tipo di evento:** scattano per allenamenti, partite ed eventi extra-campo (per esempio una cena). Il turno palloni (4), invece, esiste solo per @@ -146,16 +148,16 @@ Sono l'effetto attuale della logica, non scelte documentate altrove: ### Test che verificano il catalogo -| Cosa | Test | -| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Promemoria 24 e 3 ore: destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts` | -| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | -| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | -| 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·12·6 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`; palloni a 3 ore: DD-042, `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` | -| 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`) | +| Cosa | Test | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promemoria 24 e 3 ore (funzione ancora presente ma non pianificata, M28): destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts`; niente job pianificati: `notifiche-automatiche.test.ts` (M28) | +| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | +| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | +| 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·12·6 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`; palloni a 3 ore: DD-042, `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` | +| 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`) | Le push dei test vanno a un servizio push finto in locale: si verifica **a chi** arrivano, non la consegna su un telefono. Non ha un test l'esclusione dell'allenatore dalle **notifiche smart**: la @@ -287,7 +289,7 @@ job `pg_cron` come la 2, che scrive `turno_palloni_3h`/`_revocato` (`_12h`/`_6h` 1. **Messaggio admin** — `notifica-personalizzata.ts` inserisce una riga per destinatario in parallelo all'invio push esistente (a tutta la rosa attiva se `giocatoreId` è omesso). -2. **Promemoria evento** — due job `pg_cron` (ogni ora per la finestra delle 24h, ogni 15 +2. **Promemoria evento (rimosso dalla `m28`, DD-043: i job non esistono più)** — due job `pg_cron` (ogni ora per la finestra delle 24h, ogni 15 minuti per quella delle 3h) generano una notifica per evento imminente, per i convocati (o tutta la rosa attiva se l'evento non ne specifica), deduplicata dal registro `promemoria_eventi_generati` (M23, DD-037; il vincolo `UNIQUE (giocatore_id, evento_id, tipo)` @@ -325,7 +327,7 @@ ritrova per sempre nell'elenco, solo segnata come letta. ## Push dei promemoria (M24) -I promemoria a 24 e 3 ore nascono nel database (job `pg_cron`), che non può firmare né cifrare una +I promemoria a 24 e 3 ore (rimossi con la `m28`, DD-043; la coda resta valida per i tipi già esistenti) nascevano nel database (job `pg_cron`), che non può firmare né cifrare una push Web: la manda il worker `mailer/`, lo stesso che invia le email ([WORKER_EMAIL.md](../WORKER_EMAIL.md), DD-038). diff --git a/src/lib/notifiche-utente.ts b/src/lib/notifiche-utente.ts index 626cf36..8310b0f 100644 --- a/src/lib/notifiche-utente.ts +++ b/src/lib/notifiche-utente.ts @@ -5,7 +5,7 @@ import { useGiocatoreId } from "./user-store"; /** * Centro notifiche in-app (M17): pallino sull'avatar del profilo, con il numero delle non * lette. Cinque sorgenti riempiono `notifiche_utente` (lato database, mai dal client): un - * messaggio libero dell'admin, un promemoria automatico prima di un evento (24h e 3h), il + * messaggio libero dell'admin, un promemoria automatico prima di un evento (24h e 3h, non più generato dalla M28), il * turno palloni e il sollecito presenze (questi due avviati a mano da un admin, in * parallelo alla push esistente). Qui si legge e si segna come letto, con * `supabaseNuoveTabelle` perché `types.ts` non include ancora questa tabella (vedi diff --git a/src/routes/calendario.tsx b/src/routes/calendario.tsx index 400b663..3a35251 100644 --- a/src/routes/calendario.tsx +++ b/src/routes/calendario.tsx @@ -31,7 +31,7 @@ export const Route = createFileRoute("/calendario")({ { property: "og:title", content: "Calendario squadra — CrAPP" }, { property: "og:description", - content: "Vista mensile e prossimi eventi con promemoria per la squadra.", + content: "Vista mensile e prossimi eventi per la squadra.", }, ], }), diff --git a/supabase/migrations/20261001150000_m28_niente_promemoria_evento.sql b/supabase/migrations/20261001150000_m28_niente_promemoria_evento.sql new file mode 100644 index 0000000..5c64c9f --- /dev/null +++ b/supabase/migrations/20261001150000_m28_niente_promemoria_evento.sql @@ -0,0 +1,14 @@ +-- M28 — Niente più promemoria automatico a 24 e 3 ore prima degli eventi (DD-043) +-- +-- Si tolgono solo i due job `pg_cron` di M17: da ora il database non genera più notifiche +-- `evento_promemoria_24h` e `evento_promemoria_3h`. La funzione `genera_promemoria_eventi()`, i tipi +-- ammessi dai CHECK e il registro `promemoria_eventi_generati` restano: servono per le notifiche +-- già generate e, volendo, per rimettere in funzione il promemoria con un solo `cron.schedule`. + +DO $$ +BEGIN + PERFORM cron.unschedule(jobname) + FROM cron.job + WHERE jobname IN ('promemoria-eventi-24h', 'promemoria-eventi-3h'); +END; +$$; diff --git a/test/integration/notifiche-automatiche.test.ts b/test/integration/notifiche-automatiche.test.ts index 68e2ab3..30f0251 100644 --- a/test/integration/notifiche-automatiche.test.ts +++ b/test/integration/notifiche-automatiche.test.ts @@ -5,6 +5,7 @@ * Esegue i job veri del database (`genera_avvisi_palloni`, `genera_solleciti_presenze`) su eventi * creati con orari relativi a «adesso» e verifica: * + * - che il promemoria a 24 e 3 ore non sia più pianificato (M28, DD-043); * - le finestre (palloni: un solo avviso nelle 3 ore prima; solleciti 24·12·6 ore, contigue e senza * sovrapposizione); * - i destinatari (incaricato e turno precedente; convocati senza risposta o con «forse»; mai @@ -795,6 +796,33 @@ if (!locale) { ); }); + await prova("il promemoria a 24 e 3 ore non è più pianificato (M28, DD-043)", async () => { + const contenitore = spawnSync( + "docker", + ["ps", "--filter", "name=supabase_db", "--format", "{{.Names}}"], + { encoding: "utf8" }, + ) + .stdout.trim() + .split("\n")[0]; + assert.ok(contenitore, "container del database locale non trovato"); + const esito = spawnSync( + "docker", + [ + "exec", + contenitore, + "psql", + "-U", + "postgres", + "-At", + "-c", + "select count(*) from cron.job where jobname like 'promemoria-eventi-%'", + ], + { encoding: "utf8" }, + ); + assert.equal(esito.status, 0, esito.stderr); + assert.equal(esito.stdout.trim(), "0", "nessun job di promemoria evento"); + }); + await prova("cancellare l'evento toglie registro e notifiche collegate", async () => { assert.ok((await registro(MAIN)).length > 0); await scrivi(`eventi_app?id=eq.${MAIN}`, "DELETE"); From de78431090cb5771924429aae93027038efe1b9f Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 1 Oct 2026 15:03:04 +0200 Subject: [PATCH 4/6] Toglie la foto tessera dal profilo (DD-044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il Profilo non chiede più la foto tessera e la scheda admin non la mostra. Il completamento diventa dati personali 34%, documento 33%, certificato 33%. La migration m29 elimina la colonna profili_giocatore.foto_path; le 4 foto già caricate sono state cancellate dal bucket profili-giocatore. Migration applicata in produzione. Test: pesi e sezioni di profili-core, rendering di CampiProfilo, colonna rimossa sul database locale. Documentati DD-044, modulo profilo-giocatore, DATABASE, ROADMAP e changelog. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 5 +++ docs/DATABASE.md | 8 ++-- docs/ROADMAP.md | 2 +- docs/decisions/DD-016.md | 2 +- docs/decisions/DD-017.md | 2 +- docs/decisions/DD-044.md | 38 +++++++++++++++++++ docs/decisions/README.md | 1 + docs/modules/allenatore.md | 6 +-- docs/modules/profilo-giocatore.md | 18 +++------ .../crapp/ProfiloAmministrativo.tsx | 19 +--------- src/integrations/supabase/types.ts | 3 -- src/lib/profili-core.ts | 14 ++----- src/lib/profili.ts | 2 +- src/routes/admin.tsx | 7 ---- .../20261001170000_m29_senza_foto_tessera.sql | 9 +++++ supabase/seed.sql | 10 ++--- test/README.md | 12 +++--- test/integration/foto-tessera-rimossa.test.ts | 37 ++++++++++++++++++ test/unit/profili-core.test.ts | 16 +++++--- test/unit/profili.test.ts | 4 +- test/unit/profilo-campi.test.ts | 28 ++++++++++++++ 21 files changed, 164 insertions(+), 79 deletions(-) create mode 100644 docs/decisions/DD-044.md create mode 100644 supabase/migrations/20261001170000_m29_senza_foto_tessera.sql create mode 100644 test/integration/foto-tessera-rimossa.test.ts create mode 100644 test/unit/profilo-campi.test.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bb9cba9..cadcb38 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,11 @@ versioni `0.y.z` erano pre-release. ### Rimosso +- **Foto tessera del profilo** — il giocatore non la carica più e l'admin non la vede nella scheda; il + completamento del profilo si calcola su dati personali (34%), documento (33%) e certificato (33%). Migration + `m29_senza_foto_tessera` (elimina la colonna `foto_path`); i file già caricati sono stati cancellati dal + bucket `profili-giocatore` (DD-044). Applicata in produzione il 01/10/2026, con la cancellazione dei 4 file già caricati. + - **Promemoria evento a 24 e 3 ore** — non partono più per nessun evento (allenamenti, partite, extra-campo): niente notifica, push né email. Migration `m28_niente_promemoria_evento` (tolti i due job `pg_cron`; funzione, tipi e notifiche già generate restano). Applicata in produzione il 01/10/2026 (DD-043). diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 0aa1b87..819063e 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -66,10 +66,10 @@ Colonne di `giocatori_squadra` che meritano una nota: ## Storage -| Bucket | Scopo | Note | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `profili-giocatore` | Documento d'identità, certificato medico e foto tessera, in cartelle per giocatore (`/.`). | **Privato e destinato a restare tale**: documenti e dati sanitari non devono mai avere URL pubblici (DD-016 regola 4). Il giocatore gestisce solo la propria cartella; l'admin scarica tutto con signed URL a scadenza breve. | -| `avatar-giocatori` | Foto profilo (`/avatar.jpg`). | **Pubblico**: foto informali. Qualsiasi autenticato può caricare, sostituire o eliminare un file (nessun controllo per proprietario: la maggior parte dei giocatori non ha ancora `auth_user_id`, DD-018). Letto da `src/lib/avatar-store.ts`. | +| Bucket | Scopo | Note | +| ------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `profili-giocatore` | Documento d'identità e certificato medico, in cartelle per giocatore (`/.`). | **Privato e destinato a restare tale**: documenti e dati sanitari non devono mai avere URL pubblici (DD-016 regola 4). Il giocatore gestisce solo la propria cartella; l'admin scarica tutto con signed URL a scadenza breve. | +| `avatar-giocatori` | Foto profilo (`/avatar.jpg`). | **Pubblico**: foto informali. Qualsiasi autenticato può caricare, sostituire o eliminare un file (nessun controllo per proprietario: la maggior parte dei giocatori non ha ancora `auth_user_id`, DD-018). Letto da `src/lib/avatar-store.ts`. | ## Eventi e presenze diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1afcd3e..58e9318 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -27,7 +27,7 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). - [x] Dashboard amministratore - [x] Download CSV dati - [x] Profilo Giocatore — dati personali, documento d'identità, certificato medico - (caricamento, scadenza, stato, download) e foto tessera; lo storico dei certificati + (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) diff --git a/docs/decisions/DD-016.md b/docs/decisions/DD-016.md index e01726c..66a09c0 100644 --- a/docs/decisions/DD-016.md +++ b/docs/decisions/DD-016.md @@ -16,7 +16,7 @@ Regole vincolanti: 1. **`giocatori_squadra` diventa progressivamente la source of truth** per l’anagrafica squadra. Durante la transizione, `crapp-data.ts` resta come **fallback** se il database non è disponibile o i dati non sono ancora migrati. 2. L’associazione **`auth_user_id` ↔ giocatore** è un’operazione **controllata e atomica** (es. al primo accesso da `/benvenuto`, con `UPDATE … WHERE auth_user_id IS NULL`). Il giocatore **non può modificare liberamente** `auth_user_id`; solo un admin può resettarlo in casi eccezionali. -3. I file (documento identità, certificato, foto tessera) vivono nel bucket Storage **`profili-giocatore`**, configurato come **privato**. +3. I file (documento identità, certificato; la foto tessera è stata tolta da DD-044) vivono nel bucket Storage **`profili-giocatore`**, configurato come **privato**. 4. Documenti personali e sanitari **non devono mai essere esposti tramite URL pubblici**. Accesso solo tramite client autenticato con policy RLS, o signed URL a scadenza breve per download admin. 5. Le **tabelle preesistenti non vengono modificate** (`eventi_app`, `risposte_presenze`, voti, palloni, scout, push, ecc.). Il profilo si aggancia agli ID `g1`…`g17` già in uso, senza migrare verso UUID per ora (coerente con DD-012). 6. La tabella `giocatori` (UUID) resta **invariata e non usata** dal modulo profilo. diff --git a/docs/decisions/DD-017.md b/docs/decisions/DD-017.md index 451bf9a..4ceee4f 100644 --- a/docs/decisions/DD-017.md +++ b/docs/decisions/DD-017.md @@ -15,7 +15,7 @@ Dalla dashboard amministratore, un admin può: Restano fuori, e non cambiano: -- i **file** (documento, certificato, foto): l'admin li scarica ma non li carica né li sostituisce. Un documento d'identità lo produce il suo titolare, e la catena di responsabilità deve restare leggibile; +- i **file** (documento, certificato; la foto tessera è stata tolta da DD-044): l'admin li scarica ma non li carica né li sostituisce. Un documento d'identità lo produce il suo titolare, e la catena di responsabilità deve restare leggibile; - il **giocatore**, che continua a non poter toccare i propri dati squadra. **Alternative scartate** diff --git a/docs/decisions/DD-044.md b/docs/decisions/DD-044.md new file mode 100644 index 0000000..a82236f --- /dev/null +++ b/docs/decisions/DD-044.md @@ -0,0 +1,38 @@ +# DD-044 — Via la foto tessera dal profilo + +**Data:** 1 ottobre 2026 +**Stato:** Accettata (migration `m29`, applicata in produzione il 1 ottobre 2026) + +**Contesto** +Il profilo amministrativo chiedeva al giocatore una foto formato tessera (DD-016), per il +tesseramento CSI. Non serve più: non viene richiesta nel tesseramento e resta un dato personale +da custodire. + +**Decisione** + +1. Il profilo **non ha più la foto tessera**: sparisce il campo di caricamento dal Profilo e la + voce «Foto» dalla scheda dell'admin. +2. **Completamento del profilo** su tre sezioni: dati personali 34%, documento 33%, certificato 33% + (prima 30/30/30/10). Il totale resta 100 e il widget «Completa il tuo profilo» sparisce con le + tre sezioni complete. +3. **Dati:** la migration `m29` elimina la colonna `profili_giocatore.foto_path`. I file + `/foto.*` del bucket privato `profili-giocatore` vanno cancellati con l'API + Storage (da SQL Storage lo vieta) prima della migration, perché dopo i path non sono più + leggibili. **Non toccati:** l'avatar (bucket `avatar-giocatori`), il documento e il certificato. +4. Ordine in produzione: **prima il rilascio del codice** (la versione precedente seleziona + `foto_path` e darebbe errore), poi cancellazione dei file, poi `m29`. + +**Alternative scartate** + +- Lasciare la colonna e nascondere solo l'interfaccia → scartata: resterebbe un dato personale + senza uso e senza un campo da cui aggiornarlo. +- Tenere i pesi 30/30/30 senza rinormalizzare → scartata: nessun profilo arriverebbe a 100%. + +**Conseguenze** + +- Cancellazione definitiva: le foto già caricate non si recuperano. +- Il 1 ottobre 2026 sono state cancellate in produzione le 4 foto caricate, poi la colonna. +- Le percentuali di completamento cambiano per tutti (chi aveva il 90% senza foto passa al 100%). + +**Riesame** +Se il comitato CSI dovesse richiedere di nuovo la foto, reintrodurla con una nuova decisione. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 09bb665..abe752a 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -86,6 +86,7 @@ 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-044](DD-044.md) | Via la foto tessera dal profilo | | [DD-043](DD-043.md) | Niente promemoria evento a 24 e 3 ore | | [DD-042](DD-042.md) | Turno palloni: un solo avviso a 3 ore | | [DD-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni | diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index 91ea452..ee15632 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -55,8 +55,8 @@ Un link diretto a `/profilo?tab=stagione` o `?tab=badge` apre Documenti. | Telefono | `profili_giocatore.telefono` | | | Foto profilo | bucket `avatar-giocatori` | stesso cerchio avatar e stesso upload dei giocatori | -**Non** si mostrano: indirizzo di residenza, documento d'identità, certificato medico, foto -tessera, tesseramento CSI. +**Non** si mostrano: indirizzo di residenza, documento d'identità, certificato medico, +tesseramento CSI. ### Completamento profilo @@ -164,7 +164,7 @@ giocatore). | ------------------------------------------------------------------- | ---------- | ------------------ | ----------------------------------- | | 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 | +| Documento, certificato | 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ì | diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index 0e00435..4dec796 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -44,7 +44,7 @@ Può: riga non viene eliminata, così presenze, voti, pagelle e badge della stagione restano agganciati al suo id -Non può caricare o sostituire i file altrui: documento, certificato e foto restano +Non può caricare o sostituire i file altrui: documento e certificato restano responsabilità del giocatore che li fornisce. ## Flusso utente @@ -73,7 +73,6 @@ Viene mostrata una barra di avanzamento (esempio: _Profilo completato — 85%_), - Dati personali - Documento di identità - Certificato medico -- Foto tessera Quando tutte le sezioni sono complete il widget scompare automaticamente. @@ -273,9 +272,8 @@ Lo storico non viene mantenuto nella prima versione. ### Foto tessera -Upload di una fotografia formato tessera. - -Utilizzata dagli amministratori per il tesseramento CSI. +**Rimossa** (DD-044, migration `m29`): il profilo non chiede più la foto tessera e la colonna +`foto_path` non esiste più. I file già caricati sono stati cancellati dal bucket. ### Statistiche @@ -328,7 +326,6 @@ Per ogni giocatore, nella tab Profili, vengono mostrati. - Stato del profilo - Certificato medico - Documento di identità -- Foto tessera - Stato tesseramento CSI (tesserato / da tesserare) Azioni disponibili. @@ -336,7 +333,6 @@ Azioni disponibili. - Visualizza profilo (la scheda si apre in linea nell'elenco: nessuna schermata separata) - Scarica certificato - Scarica documento -- Scarica foto tessera - 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 @@ -377,10 +373,9 @@ Ogni sezione contribuisce alla percentuale di completamento. | Sezione | Peso | | --------------------- | ---- | -| Dati personali | 30% | -| Documento di identità | 30% | -| Certificato medico | 30% | -| Foto tessera | 10% | +| Dati personali | 34% | +| Documento di identità | 33% | +| Certificato medico | 33% | Quando tutte le sezioni risultano complete il profilo raggiunge il 100%. @@ -402,7 +397,6 @@ nessun altro ne vede (vedi [Avviso certificati](#avviso-certificati)). - Gestione dati personali - Documento di identità - Certificato medico -- Foto tessera - Dashboard amministratore - Esportazione CSV CSI diff --git a/src/components/crapp/ProfiloAmministrativo.tsx b/src/components/crapp/ProfiloAmministrativo.tsx index a41c8ea..17cfdee 100644 --- a/src/components/crapp/ProfiloAmministrativo.tsx +++ b/src/components/crapp/ProfiloAmministrativo.tsx @@ -120,14 +120,12 @@ export function CampiProfilo({ sezioni, fileDocumento, fileCertificato, - fileFoto, }: { corrente: Profilo; aggiorna: (patch: Partial) => void; sezioni: Record; fileDocumento?: React.ReactNode; fileCertificato?: React.ReactNode; - fileFoto?: React.ReactNode; }) { return ( <> @@ -241,14 +239,13 @@ export function CampiProfilo({ /> {fileCertificato} - {fileFoto} ); } /** * I dati personali dell'allenatore (DD-034): gli stessi campi di `profili_giocatore`, meno - * indirizzo, documento, certificato e foto tessera. Condivisi tra Profilo e dashboard admin. + * indirizzo, documento e certificato. Condivisi tra Profilo e dashboard admin. */ export function CampiAllenatore({ corrente, @@ -469,18 +466,6 @@ export function ProfiloAmministrativo({ onCaricato={caricato("certificatoPath")} /> } - fileFoto={ - <> - - - - } />