diff --git a/docs/decisions/DD-050.md b/docs/decisions/DD-050.md new file mode 100644 index 0000000..8830585 --- /dev/null +++ b/docs/decisions/DD-050.md @@ -0,0 +1,67 @@ +# DD-050 — Avviso a admin e allenatori se una presenza cambia a meno di 6 ore dall'evento + +**Data:** 8 ottobre 2026 +**Stato:** In valutazione (solo documentata: nessun codice né migration scritti) + +**Contesto** +Un giocatore può cambiare la risposta a un evento fino all'ultimo minuto. Chi organizza (admin e allenatore) +se ne accorge solo riaprendo l'evento: un «assente» detto due ore prima di una partita può far saltare la +formazione o il numero minimo per l'allenamento. Oggi nessuna notifica avvisa chi gestisce gli eventi di una +risposta (`notifiche.md`, «Cosa non genera nessuna notifica»). + +**Decisione** + +1. **Cosa fa scattare l'avviso.** Una riga di `risposte_presenze` che cambia — nuova, modificata o ritirata — + quando mancano **da 0 a 6 ore** all'inizio di una **partita o un allenamento** (non agli altri tipi di + evento). Scatta per **qualsiasi + cambio di stato**, da qualunque valore a qualunque altro (presente, assente, forse, in ritardo, + infortunato) e anche per la prima risposta e per il ritiro; non scatta solo se lo stato resta quello di + prima (nessun cambio reale). Vale per qualunque giocatore, anche se la + modifica la fa un admin per conto suo. +2. **Chi la riceve:** gli **admin e gli allenatori attivi** (chi ha il ruolo `admin` o `allenatore` in + `user_roles` e uno slot attivo in `giocatori_squadra`), **tranne la persona a cui appartiene la presenza** + (se un admin cambia la propria, non avvisa se stesso). Chi ha il ruolo ma nessuno slot collegato non può + ricevere notifiche, perché `notifiche_utente` è per slot. +3. **Come funziona.** Un trigger `AFTER INSERT OR UPDATE OF stato OR DELETE` su `risposte_presenze` chiama una + funzione `SECURITY DEFINER` (`avvisa_presenza_modificata()`, migration `m32`). La funzione: + 1. ignora il caso in cui lo stato prima e dopo è lo stesso; + 2. legge l'evento e continua solo se `tipo` è `partita` o `allenamento` e `inizio_evento(data, ora) - now()` + sta tra 0 e 6 ore (stessa `inizio_evento()` di M26: fuso `Europe/Rome`, ora illeggibile = nessun avviso); + 3. inserisce una riga in `notifiche_utente` per ogni destinatario, con il tipo nuovo `presenza_modificata`. + + Dal database in poi il resto è già fatto: il trigger di M22/M26 accoda l'**email**, il trigger delle push + (esteso con il tipo nuovo) accoda la **push**, la riga è il **centro notifiche in-app** (DD-038). Nessuna + route dell'app e nessun codice lato client oltre al nuovo valore di `TipoNotifica`. + +4. **Testo.** Titolo `Presenza modificata: `; corpo su righe: `Evento: `, + `Data: , ore `, `Da: `, `A: `. Gli stati sono «presente», + «assente», «forse», «in ritardo», «infortunato», «nessuna risposta» (prima di una risposta nuova, o dopo + un ritiro). Non c'è un link all'evento: il pulsante «Apri CrAPP» della mail e il tocco sulla push aprono l'app. +5. **Una notifica per modifica, senza raggruppare.** `evento_id` resta NULL (come i messaggi `admin`), così la + `UNIQUE (giocatore_id, evento_id, tipo)` non si applica e due modifiche dello stesso evento non si + sovrascrivono. Contro: se la stessa persona cambia idea cinque volte, ognuno dei destinatari riceve cinque + notifiche e cinque email. +6. **Nessuna configurazione.** Né una soglia modificabile (6 ore è scritto nella funzione), né un interruttore + per ruolo: l'email e la push seguono le preferenze già esistenti di ciascuno (`preferenze_utente`, DD-036). + +**Alternative scartate** + +- **Una route dell'app invocata dal client dopo il salvataggio** (come `sollecita-presenze`) → il client + scrive su `risposte_presenze` direttamente con RLS: una route in più sarebbe aggirabile e dipenderebbe dal + fatto che l'app sia ancora aperta al momento del salvataggio. Il trigger copre ogni scrittura. +- **Raggruppare in una sola notifica per evento** (upsert con `evento_id`) → perde il dettaglio di chi ha + cambiato cosa, e l'upsert non rifa partire la push (il trigger delle push è solo `INSERT`). +- **Notificare anche chi ha il ruolo ma è il giocatore stesso** → inutile: sa già cosa ha fatto. + +**Conseguenze** + +- Nuova migration `m32` (CHECK dei tipi, due funzioni, due trigger); `docs/DATABASE.md` e il catalogo in + `notifiche.md` si aggiornano con essa. **Va applicata in produzione** con `npx supabase db push`. +- L'allenatore, che oggi non riceve nulla sulle presenze, riceve questa notifica: va aggiunta a + [allenatore.md](../modules/allenatore.md#notifiche). +- Rumore possibile a ridosso dell'evento (punto 5). Se in pratica dà fastidio, il primo ritocco è raggruppare + per evento, non togliere l'avviso. + +**Riesame** +Dopo qualche settimana d'uso: se arrivano troppe notifiche per lo stesso giocatore, raggruppare; se 6 ore +sono troppe o poche, spostare la soglia (una costante nella funzione). diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 9166d4b..f78a0f8 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -46,55 +46,56 @@ 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 | -| [DD-049](DD-049.md) | Caratteri più grandi in tutta l'app | -| [DD-048](DD-048.md) | Via la tessera dalla dashboard | -| [DD-047](DD-047.md) | Notifica di compleanno | -| [DD-046](DD-046.md) | Avviso certificati mancanti | -| [DD-045](DD-045.md) | Sollecito presenze: un solo avviso a 24 ore | -| [DD-044](DD-044.md) | Via la foto tessera dal profilo | -| [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 | +| 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-050](DD-050.md) | Avviso se una presenza cambia a meno di 6 ore dall'evento | +| [DD-049](DD-049.md) | Caratteri più grandi in tutta l'app | +| [DD-048](DD-048.md) | Via la tessera dalla dashboard | +| [DD-047](DD-047.md) | Notifica di compleanno | +| [DD-046](DD-046.md) | Avviso certificati mancanti | +| [DD-045](DD-045.md) | Sollecito presenze: un solo avviso a 24 ore | +| [DD-044](DD-044.md) | Via la foto tessera dal profilo | +| [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 | **In valutazione** diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md index 3da94a6..94a939c 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -44,6 +44,7 @@ quelle del turno palloni**, che restano solo push e in-app: DD-040). | 6 | **Sondaggio pre-partita** — «💩 Sondaggio pre-partita aperto» | Un admin, dalla pagina della partita. Route `apri-sondaggio` (vedi [Scout Live](scout-live.md)). | Push: tutti i dispositivi iscritti, tranne quelli degli allenatori. In-app ed email (tipo `sondaggio_cacche`): i giocatori attivi, allenatori esclusi, anche senza dispositivi iscritti. | sì | sì | sì | | 7 | **Notifiche smart** — badge appena sbloccato, serie a un traguardo, obiettivo di squadra tra il 90 e il 100%, badge social vinto | L'app, mentre è aperta: sono calcolate in locale da `calcolaNotifiche()`. | Chi ha l'app aperta con uno slot giocatore in rosa selezionato; **mai l'allenatore**. Nessun invio verso altri dispositivi. | no (solo locale) | no | no | | 8 | **Compleanno** — auguri al festeggiato, avviso agli altri | **Automatica**, **una volta al giorno, dalle 8:00** (fuso `Europe/Rome`) per chi compie gli anni quel giorno (DD-047, `m31`): job `pg_cron` ogni ora, funzione `genera_avvisi_compleanno()`, registro `compleanni_notificati`; tipi `compleanno_auguri` e `compleanno`, push dal worker. Nessun pulsante manuale. | **Tutti i membri attivi**, allenatori compresi: il festeggiato riceve gli auguri, gli altri l'avviso. Con più compleanni lo stesso giorno, un solo messaggio a testa (agli altri con tutti i nomi, a ogni festeggiato solo i propri auguri). | sì | sì | sì | +| 9 | **Presenza modificata** — «Presenza modificata: _Nome Cognome_» _(proposta, DD-050: non ancora implementata)_ | **Automatica**, a ogni cambio di una risposta in `risposte_presenze` (nuova, modificata o ritirata) quando mancano da 0 a 6 ore all'inizio di una partita o di un allenamento: trigger sul database, tipo `presenza_modificata`, una notifica per modifica, push dal worker. | Gli admin e gli allenatori attivi, tranne il giocatore a cui appartiene la presenza. | sì | sì | sì | Le notifiche generate dal server (righe 1-6 e 8) hanno tutti e tre i canali (DD-038), tranne il turno palloni (riga 4), che non manda email (DD-040). La differenza sta in come parte la push: quelle dei pulsanti (3 e 6) la mandano le route dell'app, quelle @@ -75,6 +76,7 @@ disattivazione). Tra `<…>` i valori che cambiano a ogni invio; le date sono `G | 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | ` · ore . Quante cacche hai fatto? Rispondi prima del fischio d'inizio.` | | 8a | Compleanno, auguri al festeggiato | `Buon compleanno, ! 🎂` | Nessuno: solo il titolo | | 8b | Compleanno, avviso agli altri | `Oggi è il compleanno di ` — con più festeggiati ` e `, o `, e `, in ordine alfabetico per nome | Nessuno: solo il titolo. L'età non compare né negli auguri né nell'avviso | +| 9 | Presenza modificata (DD-050) | `Presenza modificata: ` | `Evento: `
`Data: , ore `
`Da: `
`A: ` — stati: presente, assente, forse, in ritardo, infortunato, nessuna risposta | 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`, @@ -102,7 +104,7 @@ dall'emoji. Non entrano nel centro notifiche e non generano email. - 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. +- Un giocatore che risponde a un evento: nessun avviso a chi gestisce gli eventi, **salvo** quando la risposta cambia a meno di 6 ore da una partita o un allenamento (riga 9, DD-050). - Assenze e infortuni. - La registrazione o il collegamento di un nuovo giocatore. - Il certificato in scadenza: resta una card in Home (DD-035).