Merge develop in main: versione 1.2.2
Avvisi certificati a 30 giorni per il giocatore (DD-041), turno palloni e sollecito presenze una sola volta (DD-042, DD-045), niente promemoria evento (DD-043), niente foto tessera (DD-044). Migration m27-m30 già applicate in produzione. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
008f0d784c
44 files changed
+725
-248
No files matched your search
+6
-4
@@ -9,12 +9,14 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
|
|||||||
|
|
||||||
## Stato generale
|
## Stato generale
|
||||||
|
|
||||||
- **Versione in produzione:** 1.2.1 su `main` — notifiche su tre canali (push, in-app, email),
|
- **Versione:** 1.2.2 su `develop` (su `main`, in produzione, resta la 1.2.1 fino al merge) — notifiche
|
||||||
promemoria che non si ripetono, turno palloni e solleciti presenze automatici.
|
su tre canali, avvisi di palloni (3 ore prima) e presenze (24 ore prima) una sola volta, niente
|
||||||
|
promemoria evento, niente foto tessera, avviso certificato a 30 giorni per il giocatore.
|
||||||
- **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio
|
- **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio
|
||||||
Project Ref `hetycilxgkdmccelwerq` non si usano più.
|
Project Ref `hetycilxgkdmccelwerq` non si usano più.
|
||||||
- **Migration:** 37 file in `supabase/migrations/`, fino a `m26_notifiche_automatiche`, tutte
|
- **Migration:** 41 file in `supabase/migrations/`, fino a `m30_sollecito_solo_24h`, tutte
|
||||||
applicate in produzione (verificato con `npx supabase migration list` il 30/09/2026).
|
applicate in produzione (verificato con `npx supabase migration list` il 01/10/2026). La `m29` toglie
|
||||||
|
`foto_path`: il codice che non la legge più (1.2.2) va rilasciato.
|
||||||
- **Worker delle notifiche** (`mailer/`): in funzione su un Raspberry Pi con Docker dal 29/09/2026
|
- **Worker delle notifiche** (`mailer/`): in funzione su un Raspberry Pi con Docker dal 29/09/2026
|
||||||
([docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md)). Il codice con l'interruttore «Email» in Profilo
|
([docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md)). Il codice con l'interruttore «Email» in Profilo
|
||||||
è su `develop`, non ancora su `main`.
|
è su `develop`, non ancora su `main`.
|
||||||
|
|||||||
@@ -13,6 +13,39 @@ versioni `0.y.z` erano pre-release.
|
|||||||
|
|
||||||
## [Non rilasciato]
|
## [Non rilasciato]
|
||||||
|
|
||||||
|
## [1.2.2] - 2026-10-01
|
||||||
|
|
||||||
|
Meno notifiche automatiche e un profilo più snello: gli avvisi dei palloni e delle presenze partono una sola volta, il
|
||||||
|
promemoria degli eventi non c'è più, la foto tessera è stata tolta e l'avviso del certificato medico arriva al
|
||||||
|
giocatore un mese prima. Le migration `m27`–`m30` sono applicate in produzione dal 01/10/2026; questa versione porta il
|
||||||
|
codice dell'app che le accompagna, da rilasciare subito: la `m29` ha tolto la colonna `foto_path` che la versione
|
||||||
|
precedente ancora legge.
|
||||||
|
|
||||||
|
### 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).
|
||||||
|
|
||||||
|
### Modificato
|
||||||
|
|
||||||
|
- **Sollecito presenze: un solo avviso automatico, 24 ore prima** — a chi non ha risposto o ha risposto
|
||||||
|
«forse»; prima ne partivano tre (24, 12 e 6 ore). Il pulsante «Sollecita» non cambia. Migration
|
||||||
|
`m30_sollecito_solo_24h`, applicata in produzione il 01/10/2026 (DD-045).
|
||||||
|
|
||||||
|
- **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).
|
||||||
|
|
||||||
|
- **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
|
## [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
|
Turno palloni e sollecito presenze partono anche da soli, a orari fissi prima dell'evento, e i testi di
|
||||||
|
|||||||
+6
-6
@@ -67,8 +67,8 @@ Colonne di `giocatori_squadra` che meritano una nota:
|
|||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
| Bucket | Scopo | Note |
|
| Bucket | Scopo | Note |
|
||||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `profili-giocatore` | Documento d'identità, certificato medico e foto tessera, in cartelle per giocatore (`<giocatore_id>/<sezione>.<est>`). | **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. |
|
| `profili-giocatore` | Documento d'identità e certificato medico, in cartelle per giocatore (`<giocatore_id>/<sezione>.<est>`). | **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 (`<giocatore_id>/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`. |
|
| `avatar-giocatori` | Foto profilo (`<giocatore_id>/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
|
## Eventi e presenze
|
||||||
@@ -152,11 +152,11 @@ riservate alla service role (anon e utenti autenticati non possono chiamarle). T
|
|||||||
[modules/notifiche.md](modules/notifiche.md); le decisioni in DD-037, DD-038 e DD-040.
|
[modules/notifiche.md](modules/notifiche.md); le decisioni in DD-037, DD-038 e DD-040.
|
||||||
|
|
||||||
| Funzione (migration) | Cosa fa | Job `pg_cron` |
|
| 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), 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_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_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) |
|
| `genera_solleciti_presenze()` (M26, M30), con `..._fascia()` | Sollecito a 24 ore (M30; in M26 erano 24, 12 e 6) ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) |
|
||||||
| `inizio_evento(data, ora)` (M26) | Istante d'inizio di un evento in fuso `Europe/Rome`; `NULL` se l'ora non è valida, e l'evento viene solo escluso. | — |
|
| `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. | — |
|
| `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). | — |
|
| `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). | — |
|
||||||
|
|||||||
@@ -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à
|
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
|
in cache, senza query aggiuntive (DD-007): `src/lib/rosa.ts` aggrega ciò che è già stato
|
||||||
letto.
|
letto.
|
||||||
6. **Push solo per eventi importanti**: convocazioni, promemoria allenamento/partita, turno
|
6. **Push solo per eventi importanti**: convocazioni, turno
|
||||||
palloni, esito finale.
|
palloni, esito finale.
|
||||||
7. **Niente funzionalità pesanti**: foto, video, chat.
|
7. **Niente funzionalità pesanti**: foto, video, chat.
|
||||||
8. **Indici** sui campi usati per filtri e relazioni in ogni nuova migration.
|
8. **Indici** sui campi usati per filtri e relazioni in ogni nuova migration.
|
||||||
|
|||||||
+1
-1
@@ -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)). |
|
| **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)). |
|
| **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). |
|
| **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/`. |
|
||||||
+3
-3
@@ -27,7 +27,7 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`).
|
|||||||
- [x] Dashboard amministratore
|
- [x] Dashboard amministratore
|
||||||
- [x] Download CSV dati
|
- [x] Download CSV dati
|
||||||
- [x] Profilo Giocatore — dati personali, documento d'identità, certificato medico
|
- [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
|
resta un'estensione futura
|
||||||
- [x] Gestione tesseramenti CSI — raccolta dati, export CSV e tracciamento di chi è già
|
- [x] Gestione tesseramenti CSI — raccolta dati, export CSV e tracciamento di chi è già
|
||||||
tesserato (numero e data di tessera)
|
tesserato (numero e data di tessera)
|
||||||
@@ -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,
|
- [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
|
da un worker Docker sull'host di casa via Gmail, con interruttore «Email» in Profilo
|
||||||
([specifica](modules/notifiche-email.md), DD-036)
|
([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 — 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)
|
presenze 24 ore prima (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano; il promemoria a 24 e 3 ore è stato tolto, DD-043) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040)
|
||||||
|
|
||||||
## Prossimo
|
## Prossimo
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Worker email: installazione e gestione
|
# Worker email: installazione e gestione
|
||||||
|
|
||||||
Come si mette in funzione, si controlla e si diagnostica il worker (`mailer/`) che invia le notifiche
|
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: turno palloni a 3 ore con revoca, sollecito presenze a 24 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-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
|
[modules/notifiche.md](modules/notifiche.md#push-dei-promemoria-m24); perché è fatto così in
|
||||||
[DD-036](decisions/DD-036.md)
|
[DD-036](decisions/DD-036.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.
|
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.
|
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.
|
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).
|
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.
|
6. La tabella `giocatori` (UUID) resta **invariata e non usata** dal modulo profilo.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Dalla dashboard amministratore, un admin può:
|
|||||||
|
|
||||||
Restano fuori, e non cambiano:
|
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.
|
- il **giocatore**, che continua a non poter toccare i propri dati squadra.
|
||||||
|
|
||||||
**Alternative scartate**
|
**Alternative scartate**
|
||||||
|
|||||||
@@ -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
|
**Aggiornamento (30 settembre 2026)** — DD-040 affianca al pulsante un invio automatico (poi ridotto a uno solo, 3 ore
|
||||||
prima dell'evento, per gli eventi con un incaricato confermato; il pulsante e la route restano.
|
prima, da DD-042), per gli eventi con un incaricato confermato; il pulsante e la route restano.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin
|
# DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin
|
||||||
|
|
||||||
**Data:** 24 settembre 2026
|
**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**
|
**Contesto**
|
||||||
Il certificato medico ha una data di scadenza (`profili_giocatore.certificato_scadenza`), ma
|
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
|
- Il rinnovo non richiede nessuna azione di chiusura: cambiata la data, l'avviso si aggiorna
|
||||||
da solo.
|
da solo.
|
||||||
- Chi non apre l'app non viene avvisato: l'avviso raggiunge solo chi entra in Home.
|
- 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**
|
**Riesame**
|
||||||
Se gli avvisi in Home non bastano (certificati che scadono comunque senza essere rinnovati),
|
Se gli avvisi in Home non bastano (certificati che scadono comunque senza essere rinnovati),
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# DD-040 — Promemoria con «oggi» o «domani», turno palloni e solleciti automatici
|
# DD-040 — Promemoria con «oggi» o «domani», turno palloni e solleciti automatici
|
||||||
|
|
||||||
**Data:** 30 settembre 2026
|
**Data:** 30 settembre 2026
|
||||||
**Stato:** Accettata (implementata e verificata in locale: migration `m26`, job, route e test; la
|
**Stato:** Accettata (implementata: migration `m26`, applicata in produzione il 30 settembre 2026, job, route e test). Il sollecito presenze è poi passato a un solo avviso a 24 ore da [DD-045](DD-045.md). Il turno palloni è poi passato a un solo avviso a 3 ore da [DD-042](DD-042.md).
|
||||||
migration non è ancora applicata in produzione)
|
|
||||||
|
|
||||||
**Contesto**
|
**Contesto**
|
||||||
Quattro notifiche su sei partivano da un pulsante premuto da un admin (DD-025 per i palloni): se nessuno
|
Quattro notifiche su sei partivano da un pulsante premuto da un admin (DD-025 per i palloni): se nessuno
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 non cambiano qui (poi ridotti a uno, DD-045).
|
||||||
|
|
||||||
|
**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).
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
`<giocatore_id>/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.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# DD-045 — Sollecito presenze: un solo avviso automatico, 24 ore prima
|
||||||
|
|
||||||
|
**Data:** 1 ottobre 2026
|
||||||
|
**Stato:** Accettata (migration `m30`, applicata in produzione il 1 ottobre 2026; modifica la parte «sollecito presenze»
|
||||||
|
di [DD-040](DD-040.md))
|
||||||
|
|
||||||
|
**Contesto**
|
||||||
|
DD-040 manda il sollecito presenze a 24, 12 e 6 ore prima dell'evento: fino a tre messaggi per chi non
|
||||||
|
risponde o resta su «forse». Ne basta uno.
|
||||||
|
|
||||||
|
**Decisione**
|
||||||
|
|
||||||
|
1. Il sollecito automatico parte **una sola volta, 24 ore prima** (finestra «meno di 24 ore»), a chi in
|
||||||
|
quel momento non ha risposto o ha risposto «forse» tra i destinatari dell'evento (convocati, o tutta
|
||||||
|
la rosa attiva; mai l'allenatore). Un evento creato a meno di 24 ore dall'inizio lo riceve comunque,
|
||||||
|
al giro successivo del job. Testo e canali (push, centro notifiche, email) restano quelli di DD-040.
|
||||||
|
2. Il **pulsante «Sollecita»** e la route `sollecita-presenze` non cambiano: l'admin o l'allenatore possono
|
||||||
|
sempre rimandarlo a mano, anche dopo l'automatico.
|
||||||
|
3. **Migration `m30`:** `genera_solleciti_presenze()` chiama solo la fascia `sollecita_presenze_24h`. I tipi
|
||||||
|
`sollecita_presenze_12h` e `_6h` restano nei `CHECK` per le notifiche già generate; non ne nascono di nuovi.
|
||||||
|
|
||||||
|
**Alternative scartate**
|
||||||
|
|
||||||
|
- Mandarlo a un orario più vicino (12 o 6 ore) → scartata su indicazione esplicita: 24 ore.
|
||||||
|
- Togliere i tipi `12h` e `6h` dai `CHECK` → le righe già generate non sarebbero più valide.
|
||||||
|
|
||||||
|
**Conseguenze**
|
||||||
|
|
||||||
|
- Chi risponde «forse» o non risponde dopo il sollecito non riceve altri avvisi automatici: tocca al
|
||||||
|
pulsante manuale.
|
||||||
|
- Il massimo di mail automatiche per giocatore ed evento scende a una (il sollecito).
|
||||||
|
|
||||||
|
**Riesame**
|
||||||
|
Se il tasso di risposta cala, valutare un secondo sollecito più vicino all'evento, solo per chi non ha mai risposto.
|
||||||
@@ -47,7 +47,7 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand
|
|||||||
**Accettate**
|
**Accettate**
|
||||||
|
|
||||||
| ID | Titolo |
|
| ID | Titolo |
|
||||||
| ------------------- | ------------------------------------- |
|
| ------------------- | ------------------------------------------- |
|
||||||
| [DD-001](DD-001.md) | Indipendenza da Lovable |
|
| [DD-001](DD-001.md) | Indipendenza da Lovable |
|
||||||
| [DD-002](DD-002.md) | Sviluppo document-first |
|
| [DD-002](DD-002.md) | Sviluppo document-first |
|
||||||
| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive |
|
| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive |
|
||||||
@@ -86,6 +86,11 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand
|
|||||||
| [DD-038](DD-038.md) | Tre canali per ogni notifica |
|
| [DD-038](DD-038.md) | Tre canali per ogni notifica |
|
||||||
| [DD-039](DD-039.md) | Niente promemoria all'allenatore |
|
| [DD-039](DD-039.md) | Niente promemoria all'allenatore |
|
||||||
| [DD-040](DD-040.md) | Palloni e solleciti automatici |
|
| [DD-040](DD-040.md) | Palloni e solleciti automatici |
|
||||||
|
| [DD-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**
|
**In valutazione**
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ Un link diretto a `/profilo?tab=stagione` o `?tab=badge` apre Documenti.
|
|||||||
| Telefono | `profili_giocatore.telefono` | |
|
| Telefono | `profili_giocatore.telefono` | |
|
||||||
| Foto profilo | bucket `avatar-giocatori` | stesso cerchio avatar e stesso upload dei giocatori |
|
| 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
|
**Non** si mostrano: indirizzo di residenza, documento d'identità, certificato medico,
|
||||||
tessera, tesseramento CSI.
|
tesseramento CSI.
|
||||||
|
|
||||||
### Completamento profilo
|
### Completamento profilo
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ a lui (`usePuoGestireEventi()`).
|
|||||||
|
|
||||||
Può anche **sollecitare le presenze** di un evento, come l'admin: la route
|
Può anche **sollecitare le presenze** di un evento, come l'admin: la route
|
||||||
`sollecita-presenze.ts` accetta admin o allenatore (`richiediGestoreEventi()`), e il
|
`sollecita-presenze.ts` accetta admin o allenatore (`richiediGestoreEventi()`), e il
|
||||||
sollecito arriva solo ai giocatori che non hanno risposto, mai all'allenatore stesso. Con DD-040 si aggiungono solleciti automatici a 24, 12 e 6 ore prima dell'evento, ma il pulsante resta.
|
sollecito arriva solo ai giocatori che non hanno risposto, mai all'allenatore stesso. Con DD-040 si aggiungono un sollecito automatico 24 ore prima dell'evento, ma il pulsante resta.
|
||||||
|
|
||||||
**Restano solo admin:** dashboard `/admin`, dati e documenti dei giocatori, export CSI,
|
**Restano solo admin:** dashboard `/admin`, dati e documenti dei giocatori, export CSI,
|
||||||
notifica personalizzata, promemoria palloni, apertura sondaggio cacche, correzione delle
|
notifica personalizzata, promemoria palloni, apertura sondaggio cacche, correzione delle
|
||||||
@@ -126,8 +126,8 @@ Cosa riceve l'allenatore, notifica per notifica (il catalogo completo, con i can
|
|||||||
[notifiche.md](notifiche.md#catalogo-delle-notifiche)):
|
[notifiche.md](notifiche.md#catalogo-delle-notifiche)):
|
||||||
|
|
||||||
| Notifica | L'allenatore la riceve? | Note |
|
| 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). |
|
| 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. |
|
| 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 . |
|
| 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. |
|
| Turno palloni | no | I turni riguardano solo i giocatori. |
|
||||||
@@ -164,7 +164,7 @@ giocatore).
|
|||||||
| ------------------------------------------------------------------- | ---------- | ------------------ | ----------------------------------- |
|
| ------------------------------------------------------------------- | ---------- | ------------------ | ----------------------------------- |
|
||||||
| Modificare nome e cognome | no | sì, i propri | tutti |
|
| Modificare nome e cognome | no | sì, i propri | tutti |
|
||||||
| Dati personali e foto profilo | i propri | i propri (ridotti) | tutti (non i file) |
|
| 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 |
|
| Avviso certificati in Home | il proprio | no | tutta la rosa |
|
||||||
| Creare/modificare/eliminare eventi, convocati | no | sì | sì |
|
| Creare/modificare/eliminare eventi, convocati | no | sì | sì |
|
||||||
| Sollecitare le presenze | no | sì | sì |
|
| Sollecitare le presenze | no | sì | sì |
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ App su Vercel ──scrive──▶ notifiche_utente ──trigger──▶ noti
|
|||||||
|
|
||||||
## Quali notifiche e a chi
|
## Quali notifiche e a chi
|
||||||
|
|
||||||
**Ogni riga nuova di `notifiche_utente` genera una mail**: messaggio admin, promemoria a 24 ore e a
|
**Ogni riga nuova di `notifiche_utente` genera una mail**: messaggio admin, sollecito presenze, sondaggio pre-partita. **Fa eccezione il turno palloni** (DD-040, dalla
|
||||||
3 ore, 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
|
`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
|
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
|
viaggerebbe per email senza altro lavoro. Le notifiche già presenti quando si applica la migration
|
||||||
|
|||||||
+28
-27
@@ -35,24 +35,24 @@ in-app** (storico in `notifiche_utente`, per tutti i giocatori) ed **email**
|
|||||||
quelle del turno palloni**, che restano solo push e in-app: DD-040).
|
quelle del turno palloni**, che restano solo push e in-app: DD-040).
|
||||||
|
|
||||||
| # | Notifica | Cosa la fa partire | Destinatari | Push | In-app | Email |
|
| # | 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ì |
|
| 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** — 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ì |
|
| 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ì |
|
| 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) |
|
| 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ì |
|
| 5 | **Sollecito presenze** — «Conferma di partecipazione richiesta: _titolo_» | **Automatica**, **una sola volta, 24 ore prima** dell'evento (DD-040, DD-045): per gli eventi che iniziano entro 24 ore, dallo stesso job; tipo `sollecita_presenze_24h` (`_12h` e `_6h` esistono solo nelle notifiche generate prima di `m30`), push dal worker. **Manuale**, in più: un admin o un allenatore con il pulsante nella pagina dell'evento; route `sollecita-presenze`, tipo `sollecita_presenze`, push dalla route. | Automatica e manuale: i destinatari dell'evento (convocati, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori) che in quel momento non hanno risposto o hanno risposto «forse». | sì | sì | sì |
|
||||||
| 6 | **Sondaggio pre-partita** — «💩 Sondaggio pre-partita aperto» | Un admin, dalla pagina della partita. Route `apri-sondaggio` (vedi [Scout Live](scout-live.md)). | Push: tutti i dispositivi iscritti, tranne quelli degli allenatori. In-app ed email (tipo `sondaggio_cacche`): i giocatori attivi, allenatori esclusi, anche senza dispositivi iscritti. | sì | sì | sì |
|
| 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 |
|
| 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
|
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
|
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
|
promemoria»). Il sondaggio riscrive la notifica esistente se l'admin preme di nuovo il pulsante per
|
||||||
lo stesso evento (vedi «Centro notifiche in-app»).
|
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
|
> **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
|
> applicate in produzione (`m26` il 30/09/2026; `m27` il 01/10/2026, che porta il turno palloni a un solo
|
||||||
> come in 1.2.0: promemoria con il testo breve, turno palloni e sollecito presenze solo da pulsante.
|
> avviso 3 ore prima, DD-042).
|
||||||
|
|
||||||
Cosa riceve l'allenatore, notifica per notifica: [allenatore.md](allenatore.md#notifiche).
|
Cosa riceve l'allenatore, notifica per notifica: [allenatore.md](allenatore.md#notifiche).
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@ volta sola. Nell'email il titolo diventa l'oggetto e il corpo segue il formato d
|
|||||||
disattivazione). Tra `<…>` i valori che cambiano a ogni invio; le date sono `GG/MM/AAAA`.
|
disattivazione). Tra `<…>` i valori che cambiano a ogni invio; le date sono `GG/MM/AAAA`.
|
||||||
|
|
||||||
| # | Notifica | Titolo | Corpo |
|
| # | Notifica | Titolo | Corpo |
|
||||||
| --- | ----------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --- | --------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 1-2 | Promemoria 24 e 3 ore | `Promemoria evento di domani: <titolo evento>`, oppure `…di oggi: <titolo evento>` | `Data: domani, <GG/MM/AAAA>`<br>`Ora: <ora>`<br>`Luogo: <luogo>` — «domani» se la data dell'evento è quella di domani, «oggi» altrimenti (fuso `Europe/Rome`, vale per entrambi i promemoria) |
|
| 1-2 | Promemoria 24 e 3 ore (rimosso, DD-043) | `Promemoria evento di domani: <titolo evento>`, oppure `…di oggi: <titolo evento>` | `Data: domani, <GG/MM/AAAA>`<br>`Ora: <ora>`<br>`Luogo: <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. |
|
| 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: <titolo>`<br>`Data: <GG/MM/AAAA>, ore <ora>`<br>`Incarico: custodia dei palloni al termine dell'evento`<br>`Riconsegna: <GG/MM/AAAA dell'evento successivo>` — senza evento successivo la riga «Riconsegna» manca |
|
| 4a | Turno palloni, all'incaricato | `Turno palloni: incarico assegnato` | `Evento: <titolo>`<br>`Data: <GG/MM/AAAA>, ore <ora>`<br>`Incarico: custodia dei palloni al termine dell'evento`<br>`Riconsegna: <GG/MM/AAAA dell'evento successivo>` — senza evento successivo la riga «Riconsegna» manca |
|
||||||
| 4b | Turno palloni, a chi li aveva | `Turno palloni: riconsegna` | `Evento: <titolo>`<br>`Data: <GG/MM/AAAA>, ore <ora>`<br>`Incarico: riconsegna dei palloni in custodia dal turno precedente` |
|
| 4b | Turno palloni, a chi li aveva | `Turno palloni: riconsegna` | `Evento: <titolo>`<br>`Data: <GG/MM/AAAA>, ore <ora>`<br>`Incarico: riconsegna dei palloni in custodia dal turno precedente` |
|
||||||
@@ -73,7 +73,7 @@ disattivazione). Tra `<…>` i valori che cambiano a ogni invio; le date sono `G
|
|||||||
| 5 | Sollecito presenze | `Conferma di partecipazione richiesta: <titolo>` | `Data: <GG/MM/AAAA>`<br>`Ora: <ora>`<br>`Luogo: <luogo>`<br>`Richiesta di: <Nome>` (solo nel sollecito manuale, e solo se il nome c'è)<br>`Risposta attuale: nessuna` (oppure `forse`)<br>`Azione: indicare presente, assente o in ritardo` — stesso testo a 24, 12 e 6 ore |
|
| 5 | Sollecito presenze | `Conferma di partecipazione richiesta: <titolo>` | `Data: <GG/MM/AAAA>`<br>`Ora: <ora>`<br>`Luogo: <luogo>`<br>`Richiesta di: <Nome>` (solo nel sollecito manuale, e solo se il nome c'è)<br>`Risposta attuale: nessuna` (oppure `forse`)<br>`Azione: indicare presente, assente o in ritardo` — stesso testo a 24, 12 e 6 ore |
|
||||||
| 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | `<partita> · ore <ora>. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.` |
|
| 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | `<partita> · ore <ora>. 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_promemoria_eventi`, `genera_avvisi_palloni_fascia`, `genera_revoche_palloni`,
|
||||||
`genera_solleciti_presenze_fascia`); i pulsanti usano gli stessi testi da `palloni-core.ts`
|
`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
|
(`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
|
### Cosa non genera nessuna notifica
|
||||||
|
|
||||||
- Creare, spostare, modificare o cancellare un evento: contano solo i promemoria prima
|
- Creare, spostare, modificare o cancellare un evento. Dalla `m28` (DD-043) non parte nemmeno più un
|
||||||
dell'inizio (DD-030).
|
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.
|
- 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.
|
||||||
- Assenze e infortuni.
|
- Assenze e infortuni.
|
||||||
@@ -106,7 +106,9 @@ dall'emoji. Non entrano nel centro notifiche e non generano email.
|
|||||||
|
|
||||||
### Comportamenti da conoscere
|
### 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
|
- **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
|
eventi extra-campo (per esempio una cena). Il turno palloni (4), invece, esiste solo per
|
||||||
@@ -134,12 +136,12 @@ Sono l'effetto attuale della logica, non scelte documentate altrove:
|
|||||||
proposta automatica di `completaTurni()` non salvata non basta. Se l'incaricato cambia dopo
|
proposta automatica di `completaTurni()` non salvata non basta. Se l'incaricato cambia dopo
|
||||||
un avviso, il nuovo riceve il proprio al giro successivo e il precedente riceve una sola volta
|
un avviso, il nuovo riceve il proprio al giro successivo e il precedente riceve una sola volta
|
||||||
«Turno palloni: incarico revocato»; se viene poi riassegnato, non riceve un secondo avviso.
|
«Turno palloni: incarico revocato»; se viene poi riassegnato, non riceve un secondo avviso.
|
||||||
- **I solleciti cadono in finestre che non si sovrappongono** (DD-040): 24 ore tra 24 e 12 ore
|
- **Il sollecito automatico è uno solo, a 24 ore** (DD-045): parte per gli eventi che iniziano entro 24
|
||||||
prima dell'inizio, 12 ore tra 12 e 6, 6 ore sotto le 6. Un evento creato a meno di 12 ore
|
ore, a chi in quel momento non ha risposto o ha risposto «forse». Un evento creato a meno di 24
|
||||||
dall'inizio riceve solo quello della sua finestra. Chi risponde presente, assente o in ritardo
|
ore dall'inizio lo riceve comunque, una volta. Chi risponde dopo non ne riceve altri; chi resta su
|
||||||
smette di riceverli; chi resta su «forse» li riceve tutti e tre.
|
«forse» non lo riceve una seconda volta. Altri solleciti solo col pulsante manuale.
|
||||||
- **Per ogni evento un giocatore riceve al massimo cinque mail automatiche** (DD-040): due
|
- **Per ogni evento un giocatore riceve al massimo una mail automatica** (DD-045): il
|
||||||
promemoria e tre solleciti, più quelle dei pulsanti manuali (sollecito, sondaggio). Il turno palloni
|
sollecito presenze (il promemoria è stato tolto, DD-043), più quelle dei pulsanti manuali (sollecito, sondaggio). Il turno palloni
|
||||||
(avvisi automatici, revoca e pulsante) non manda email: restano la push e l'alert in app.
|
(avvisi automatici, revoca e pulsante) non manda email: restano la push e l'alert in app.
|
||||||
- **Non esistono preferenze per tipo di notifica:** un solo interruttore per la push («Notifiche»)
|
- **Non esistono preferenze per tipo di notifica:** un solo interruttore per la push («Notifiche»)
|
||||||
e uno per le email («Email»). Il centro notifiche in-app non si può spegnere.
|
e uno per le email («Email»). Il centro notifiche in-app non si può spegnere.
|
||||||
@@ -147,13 +149,13 @@ Sono l'effetto attuale della logica, non scelte documentate altrove:
|
|||||||
### Test che verificano il catalogo
|
### Test che verificano il catalogo
|
||||||
|
|
||||||
| Cosa | Test |
|
| 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` |
|
| 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` |
|
| 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` |
|
| 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` |
|
| 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` |
|
| 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` |
|
| Promemoria con «oggi/domani», turno palloni a 3 ore e revoca, solleciti a 24 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`; palloni a 3 ore: DD-042, solleciti a 24 ore: DD-045, `m30`; `m27`) | `integration/notifiche-automatiche.test.ts`, `promemoria-eventi.test.ts` (testo oggi/domani), `destinatari-notifiche.test.ts` (pulsanti), `unit/palloni-core.test.ts`, `unit/presenze.test.ts` |
|
||||||
| Worker: esiti, backoff, tetto giornaliero, iscrizioni scadute | `unit/mailer-core.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`) |
|
| L'allenatore è fuori dalla rosa di gioco | `unit/giocatori-squadra.test.ts` (`inRosa`) |
|
||||||
|
|
||||||
@@ -282,12 +284,11 @@ non richiede che il dispositivo abbia attivato «Notifiche», ha uno storico per
|
|||||||
stessa cosa delle notifiche smart (quelle restano locali, non salvate a database).
|
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
|
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` solo fino a `m29`); i pulsanti restano e scrivono `turno_palloni` e `sollecita_presenze`):
|
||||||
`_12h`/`_6h`; i pulsanti restano e scrivono `turno_palloni` e `sollecita_presenze`):
|
|
||||||
|
|
||||||
1. **Messaggio admin** — `notifica-personalizzata.ts` inserisce una riga per destinatario
|
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).
|
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
|
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
|
(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)`
|
`promemoria_eventi_generati` (M23, DD-037; il vincolo `UNIQUE (giocatore_id, evento_id, tipo)`
|
||||||
@@ -325,7 +326,7 @@ ritrova per sempre nell'elenco, solo segnata come letta.
|
|||||||
|
|
||||||
## Push dei promemoria (M24)
|
## 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),
|
push Web: la manda il worker `mailer/`, lo stesso che invia le email ([WORKER_EMAIL.md](../WORKER_EMAIL.md),
|
||||||
DD-038).
|
DD-038).
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
Gestire un turno a rotazione condiviso per chi porta e riporta i palloni ad allenamenti e
|
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
|
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.
|
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`
|
## Route API pubblica `/api/public/promemoria-palloni`
|
||||||
|
|
||||||
> **Affiancata da un invio automatico (DD-040, migration `m26`).** Oltre al pulsante, un job
|
> **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
|
> 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» /
|
> `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
|
> «riconsegna», validi per il pulsante e per i job, più la revoca se l'incaricato cambia) sono nel
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ src/routes/api/public/sollecita-presenze.ts
|
|||||||
|
|
||||||
> **Sollecito automatico (DD-040, migration `m26`).** Il pulsante «Sollecita» e la route
|
> **Sollecito automatico (DD-040, migration `m26`).** Il pulsante «Sollecita» e la route
|
||||||
> **restano**, ma `destinatariSollecito()` passa a guardare anche i convocati (convocati che non hanno
|
> **restano**, ma `destinatariSollecito()` passa a guardare anche i convocati (convocati che non hanno
|
||||||
> risposto o hanno risposto «forse», o tutta la rosa se `convocati` è vuoto); in più un job `pg_cron` manda il sollecito **24, 12 e 6 ore prima** dell'evento a chi in
|
> risposto o hanno risposto «forse», o tutta la rosa se `convocati` è vuoto); in più un job `pg_cron` manda il sollecito **una sola volta, 24 ore prima** dell'evento (DD-045, migration `m30`) a chi in
|
||||||
> quel momento non ha risposto o ha risposto «forse», tra i destinatari dell'evento (convocati, o
|
> quel momento non ha risposto o ha risposto «forse», tra i destinatari dell'evento (convocati, o
|
||||||
> tutta la rosa attiva; mai l'allenatore). Testo e finestre nel [catalogo](notifiche.md#catalogo-delle-notifiche).
|
> tutta la rosa attiva; mai l'allenatore). Testo e finestre nel [catalogo](notifiche.md#catalogo-delle-notifiche).
|
||||||
> Il diagramma sopra è quello del pulsante, che da DD-040 legge anche `convocati` dell'evento.
|
> Il diagramma sopra è quello del pulsante, che da DD-040 legge anche `convocati` dell'evento.
|
||||||
@@ -94,7 +94,7 @@ vale per tutta la rosa) — `eventiContanoPresenze()` in `presenze.ts`.
|
|||||||
|
|
||||||
- Aggiornamento ottimistico della cache locale dopo ogni salvataggio: nessuna rilettura dal
|
- Aggiornamento ottimistico della cache locale dopo ogni salvataggio: nessuna rilettura dal
|
||||||
server, la UI risponde subito.
|
server, la UI risponde subito.
|
||||||
- Il sollecito ha due vie (DD-040): tre solleciti **automatici** (24, 12 e 6 ore prima, job `pg_cron`
|
- Il sollecito ha due vie (DD-040): un sollecito **automatico** (24 ore prima, job `pg_cron`
|
||||||
nel database) e il pulsante **manuale** accanto, che non sostituiscono. La route del pulsante verifica
|
nel database) e il pulsante **manuale** accanto, che non sostituiscono. La route del pulsante verifica
|
||||||
il ruolo lato server con `richiediGestoreEventi` (admin o allenatore, DD-024, DD-034).
|
il ruolo lato server con `richiediGestoreEventi` (admin o allenatore, DD-024, DD-034).
|
||||||
- Ognuno risponde **solo per sé**, e non è più una regola della sola interfaccia: dalla
|
- Ognuno risponde **solo per sé**, e non è più una regola della sola interfaccia: dalla
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ Può:
|
|||||||
riga non viene eliminata, così presenze, voti, pagelle e badge della stagione restano
|
riga non viene eliminata, così presenze, voti, pagelle e badge della stagione restano
|
||||||
agganciati al suo id
|
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.
|
responsabilità del giocatore che li fornisce.
|
||||||
|
|
||||||
## Flusso utente
|
## Flusso utente
|
||||||
@@ -73,7 +73,6 @@ Viene mostrata una barra di avanzamento (esempio: _Profilo completato — 85%_),
|
|||||||
- Dati personali
|
- Dati personali
|
||||||
- Documento di identità
|
- Documento di identità
|
||||||
- Certificato medico
|
- Certificato medico
|
||||||
- Foto tessera
|
|
||||||
|
|
||||||
Quando tutte le sezioni sono complete il widget scompare automaticamente.
|
Quando tutte le sezioni sono complete il widget scompare automaticamente.
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ sulla tab Stagione.
|
|||||||
|
|
||||||
### Avviso certificati
|
### 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
|
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
|
accorge quando il giocatore è già fuori regola. Si calcola al volo dalla data di scadenza
|
||||||
@@ -91,10 +90,10 @@ già salvata: niente tabella, niente migration, niente push.
|
|||||||
#### Chi lo vede
|
#### Chi lo vede
|
||||||
|
|
||||||
| Utente | Cosa vede | Al tocco |
|
| Utente | Cosa vede | Al tocco |
|
||||||
| --------------------------- | --------------------------------------------------------------- | ----------------------------- |
|
| --------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------- |
|
||||||
| Admin | l'avviso **dello staff**: i nomi di tutti i giocatori coinvolti | niente, non è cliccabile |
|
| 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` |
|
| 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 |
|
| 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 | — |
|
| Altri giocatori | niente | — |
|
||||||
| Allenatore | niente: non vede i certificati altrui e non ne ha uno (DD-034) | — |
|
| Allenatore | niente: non vede i certificati altrui e non ne ha uno (DD-034) | — |
|
||||||
|
|
||||||
@@ -109,16 +108,25 @@ solo proprio a un giocatore.
|
|||||||
|
|
||||||
#### Quando compare
|
#### Quando compare
|
||||||
|
|
||||||
`giorni` è la differenza in giorni di calendario tra la scadenza e oggi (ora locale), soglia
|
`giorni` è la differenza in giorni di calendario tra la scadenza e oggi (ora locale). La
|
||||||
`GIORNI_AVVISO_CERTIFICATO = 7`.
|
soglia del giallo dipende da chi guarda (DD-041): il giocatore è avvisato un mese prima dello
|
||||||
|
staff.
|
||||||
|
|
||||||
|
| Chi guarda | Soglia |
|
||||||
|
| ---------- | ------------------------------------------ |
|
||||||
|
| Giocatore | `GIORNI_AVVISO_CERTIFICATO_GIOCATORE = 30` |
|
||||||
|
| Admin | `GIORNI_AVVISO_CERTIFICATO = 7` |
|
||||||
|
|
||||||
| Condizione | Stato | Avviso |
|
| Condizione | Stato | Avviso |
|
||||||
| -------------------- | ----------- | ------------------ |
|
| --------------------- | ----------- | ------------------ |
|
||||||
| `giorni > 7` | valido | nessuno |
|
| `giorni > soglia` | valido | nessuno |
|
||||||
| `0 ≤ giorni ≤ 7` | in scadenza | giallo (`warning`) |
|
| `0 ≤ giorni ≤ soglia` | in scadenza | giallo (`warning`) |
|
||||||
| `giorni < 0` | scaduto | nero (`primary`) |
|
| `giorni < 0` | scaduto | nero (`primary`) |
|
||||||
| data o file mancanti | mancante | nessuno |
|
| 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
|
- 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.
|
giocatore compare nel giallo con «scade oggi» e passa al nero dal giorno dopo.
|
||||||
- L'avviso nero resta finché il giocatore non aggiorna la data: non c'è un limite di tempo.
|
- L'avviso nero resta finché il giocatore non aggiorna la data: non c'è un limite di tempo.
|
||||||
@@ -136,7 +144,7 @@ solo proprio a un giocatore.
|
|||||||
| Giallo, personale | «Certificato in scadenza» | «Il tuo certificato medico scade tra 5 giorni» / «domani» / «oggi» |
|
| 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» |
|
| 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.
|
conserva niente, rilegge la data a ogni apertura della Home.
|
||||||
|
|
||||||
#### Più giocatori
|
#### Più giocatori
|
||||||
@@ -183,10 +191,12 @@ la Home non deve rompersi per un dato accessorio.
|
|||||||
#### Implementazione
|
#### Implementazione
|
||||||
|
|
||||||
| Pezzo | Ruolo |
|
| Pezzo | Ruolo |
|
||||||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `GIORNI_AVVISO_CERTIFICATO` (`profili-core.ts`) | la soglia, 7 |
|
| `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 |
|
| `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 }` |
|
| `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» |
|
| `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/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 `<AvvisoCertificati />` subito dopo `<PromemoriaPalloni />` |
|
| `src/routes/index.tsx` | monta `<AvvisoCertificati />` subito dopo `<PromemoriaPalloni />` |
|
||||||
@@ -197,7 +207,7 @@ la Home non deve rompersi per un dato accessorio.
|
|||||||
`/admin`, quindi al massimo una lettura di `profili_giocatore` per sessione (vedi
|
`/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
|
[EFFICIENZA_CLOUD.md](../EFFICIENZA_CLOUD.md)). Per l'allenatore il componente non legge
|
||||||
i profili.
|
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,
|
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
|
ordinamento (per data, poi alfabetico), testi al singolare e al plurale, cambio di mese e
|
||||||
di anno nel calcolo dei giorni.
|
di anno nel calcolo dei giorni.
|
||||||
@@ -254,17 +264,16 @@ Upload.
|
|||||||
|
|
||||||
Il giocatore può aggiornare liberamente sia la data sia il file.
|
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
|
Quando mancano 30 giorni o meno alla scadenza, o il certificato è scaduto, il giocatore
|
||||||
riceve un avviso nella propria Home e compare in quello degli admin (vedi
|
riceve un avviso nella propria Home; compare in quello degli admin da 7 giorni prima (vedi
|
||||||
[Avviso certificati](#avviso-certificati)).
|
[Avviso certificati](#avviso-certificati)).
|
||||||
|
|
||||||
Lo storico non viene mantenuto nella prima versione.
|
Lo storico non viene mantenuto nella prima versione.
|
||||||
|
|
||||||
### Foto tessera
|
### Foto tessera
|
||||||
|
|
||||||
Upload di una fotografia formato tessera.
|
**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.
|
||||||
Utilizzata dagli amministratori per il tesseramento CSI.
|
|
||||||
|
|
||||||
### Statistiche
|
### Statistiche
|
||||||
|
|
||||||
@@ -317,7 +326,6 @@ Per ogni giocatore, nella tab Profili, vengono mostrati.
|
|||||||
- Stato del profilo
|
- Stato del profilo
|
||||||
- Certificato medico
|
- Certificato medico
|
||||||
- Documento di identità
|
- Documento di identità
|
||||||
- Foto tessera
|
|
||||||
- Stato tesseramento CSI (tesserato / da tesserare)
|
- Stato tesseramento CSI (tesserato / da tesserare)
|
||||||
|
|
||||||
Azioni disponibili.
|
Azioni disponibili.
|
||||||
@@ -325,7 +333,6 @@ Azioni disponibili.
|
|||||||
- Visualizza profilo (la scheda si apre in linea nell'elenco: nessuna schermata separata)
|
- Visualizza profilo (la scheda si apre in linea nell'elenco: nessuna schermata separata)
|
||||||
- Scarica certificato
|
- Scarica certificato
|
||||||
- Scarica documento
|
- Scarica documento
|
||||||
- Scarica foto tessera
|
|
||||||
- Modifica dati squadra e dati personali del giocatore (DD-017)
|
- Modifica dati squadra e dati personali del giocatore (DD-017)
|
||||||
- Registra numero e data della tessera CSI, una volta arrivata dal comitato
|
- Registra numero e data della tessera CSI, una volta arrivata dal comitato
|
||||||
- Scollega account, per liberare uno slot assegnato per errore
|
- Scollega account, per liberare uno slot assegnato per errore
|
||||||
@@ -366,10 +373,9 @@ Ogni sezione contribuisce alla percentuale di completamento.
|
|||||||
|
|
||||||
| Sezione | Peso |
|
| Sezione | Peso |
|
||||||
| --------------------- | ---- |
|
| --------------------- | ---- |
|
||||||
| Dati personali | 30% |
|
| Dati personali | 34% |
|
||||||
| Documento di identità | 30% |
|
| Documento di identità | 33% |
|
||||||
| Certificato medico | 30% |
|
| Certificato medico | 33% |
|
||||||
| Foto tessera | 10% |
|
|
||||||
|
|
||||||
Quando tutte le sezioni risultano complete il profilo raggiunge il 100%.
|
Quando tutte le sezioni risultano complete il profilo raggiunge il 100%.
|
||||||
|
|
||||||
@@ -391,7 +397,6 @@ nessun altro ne vede (vedi [Avviso certificati](#avviso-certificati)).
|
|||||||
- Gestione dati personali
|
- Gestione dati personali
|
||||||
- Documento di identità
|
- Documento di identità
|
||||||
- Certificato medico
|
- Certificato medico
|
||||||
- Foto tessera
|
|
||||||
- Dashboard amministratore
|
- Dashboard amministratore
|
||||||
- Esportazione CSV CSI
|
- Esportazione CSV CSI
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "crapp",
|
"name": "crapp",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AlertCircle } from "lucide-react";
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useProfili } from "@/lib/profili";
|
import { useProfili } from "@/lib/profili";
|
||||||
import {
|
import {
|
||||||
avvisiCertificati,
|
avvisiCertificatiUtente,
|
||||||
formatDataBreve,
|
formatDataBreve,
|
||||||
testoScadenza,
|
testoScadenza,
|
||||||
type AvvisoCertificato,
|
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() {
|
export function AvvisoCertificati() {
|
||||||
const admin = useIsAdmin();
|
const admin = useIsAdmin();
|
||||||
const ruoliPronti = useRuoliPronti();
|
const ruoliPronti = useRuoliPronti();
|
||||||
@@ -61,11 +64,33 @@ export function AvvisoCertificati() {
|
|||||||
|
|
||||||
const oggi = oggiISO();
|
const oggi = oggiISO();
|
||||||
|
|
||||||
if (admin) {
|
// Admin che è anche giocatore: vede il proprio avviso a 30 giorni e quello dello staff (DD-041).
|
||||||
const { scaduti, inScadenza } = avvisiCertificati(rosa, profili, oggi);
|
const { personale, staff } = avvisiCertificatiUtente(rosa, profili, oggi, { admin, base });
|
||||||
if (scaduti.length === 0 && inScadenza.length === 0) return null;
|
const cardPersonale = personale ? (
|
||||||
|
personale.giorni < 0 ? (
|
||||||
|
<Card
|
||||||
|
colore="primary"
|
||||||
|
titolo="Certificato scaduto"
|
||||||
|
cliccabile
|
||||||
|
righe={[
|
||||||
|
`Il tuo certificato medico è scaduto il ${formatDataBreve(personale.scadenza)}: caricane uno nuovo`,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
colore="warning"
|
||||||
|
titolo="Certificato in scadenza"
|
||||||
|
cliccabile
|
||||||
|
righe={[`Il tuo certificato medico ${testoScadenza(personale.giorni)}`]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
if (!staff) return cardPersonale;
|
||||||
|
const { scaduti, inScadenza } = staff;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{cardPersonale}
|
||||||
{scaduti.length > 0 ? (
|
{scaduti.length > 0 ? (
|
||||||
<Card
|
<Card
|
||||||
colore="primary"
|
colore="primary"
|
||||||
@@ -82,30 +107,6 @@ export function AvvisoCertificati() {
|
|||||||
) : null}
|
) : 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 ? (
|
|
||||||
<Card
|
|
||||||
colore="primary"
|
|
||||||
titolo="Certificato scaduto"
|
|
||||||
cliccabile
|
|
||||||
righe={[
|
|
||||||
`Il tuo certificato medico è scaduto il ${formatDataBreve(mio.scadenza)}: caricane uno nuovo`,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Card
|
|
||||||
colore="warning"
|
|
||||||
titolo="Certificato in scadenza"
|
|
||||||
cliccabile
|
|
||||||
righe={[`Il tuo certificato medico ${testoScadenza(mio.giorni)}`]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rigaStaff(a: AvvisoCertificato, dettaglio: string): string {
|
function rigaStaff(a: AvvisoCertificato, dettaglio: string): string {
|
||||||
|
|||||||
@@ -120,14 +120,12 @@ export function CampiProfilo({
|
|||||||
sezioni,
|
sezioni,
|
||||||
fileDocumento,
|
fileDocumento,
|
||||||
fileCertificato,
|
fileCertificato,
|
||||||
fileFoto,
|
|
||||||
}: {
|
}: {
|
||||||
corrente: Profilo;
|
corrente: Profilo;
|
||||||
aggiorna: (patch: Partial<Profilo>) => void;
|
aggiorna: (patch: Partial<Profilo>) => void;
|
||||||
sezioni: Record<Sezione, boolean>;
|
sezioni: Record<Sezione, boolean>;
|
||||||
fileDocumento?: React.ReactNode;
|
fileDocumento?: React.ReactNode;
|
||||||
fileCertificato?: React.ReactNode;
|
fileCertificato?: React.ReactNode;
|
||||||
fileFoto?: React.ReactNode;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -241,14 +239,13 @@ export function CampiProfilo({
|
|||||||
/>
|
/>
|
||||||
</Campo>
|
</Campo>
|
||||||
{fileCertificato}
|
{fileCertificato}
|
||||||
{fileFoto}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I dati personali dell'allenatore (DD-034): gli stessi campi di `profili_giocatore`, meno
|
* 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({
|
export function CampiAllenatore({
|
||||||
corrente,
|
corrente,
|
||||||
@@ -469,18 +466,6 @@ export function ProfiloAmministrativo({
|
|||||||
onCaricato={caricato("certificatoPath")}
|
onCaricato={caricato("certificatoPath")}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
fileFoto={
|
|
||||||
<>
|
|
||||||
<Intestazione titolo="Foto tessera" completa={sezioni.foto} />
|
|
||||||
<CampoFile
|
|
||||||
label="Foto tessera"
|
|
||||||
path={corrente.fotoPath}
|
|
||||||
sezione="foto"
|
|
||||||
giocatoreId={giocatoreId}
|
|
||||||
onCaricato={caricato("fotoPath")}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -550,7 +535,7 @@ export function CompletaProfilo({
|
|||||||
<p className="mt-2 text-[13px] text-muted-foreground">
|
<p className="mt-2 text-[13px] text-muted-foreground">
|
||||||
{allenatore
|
{allenatore
|
||||||
? "Data e luogo di nascita, telefono ed email."
|
? "Data e luogo di nascita, telefono ed email."
|
||||||
: "Documento, certificato medico e foto tessera servono per il tesseramento CSI."}
|
: "Documento e certificato medico servono per il tesseramento CSI."}
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|||||||
@@ -375,7 +375,6 @@ export type Database = {
|
|||||||
documento_scadenza: string | null;
|
documento_scadenza: string | null;
|
||||||
documento_tipo: string | null;
|
documento_tipo: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
foto_path: string | null;
|
|
||||||
giocatore_id: string;
|
giocatore_id: string;
|
||||||
indirizzo: string | null;
|
indirizzo: string | null;
|
||||||
luogo_nascita: string | null;
|
luogo_nascita: string | null;
|
||||||
@@ -395,7 +394,6 @@ export type Database = {
|
|||||||
documento_scadenza?: string | null;
|
documento_scadenza?: string | null;
|
||||||
documento_tipo?: string | null;
|
documento_tipo?: string | null;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
foto_path?: string | null;
|
|
||||||
giocatore_id: string;
|
giocatore_id: string;
|
||||||
indirizzo?: string | null;
|
indirizzo?: string | null;
|
||||||
luogo_nascita?: string | null;
|
luogo_nascita?: string | null;
|
||||||
@@ -415,7 +413,6 @@ export type Database = {
|
|||||||
documento_scadenza?: string | null;
|
documento_scadenza?: string | null;
|
||||||
documento_tipo?: string | null;
|
documento_tipo?: string | null;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
foto_path?: string | null;
|
|
||||||
giocatore_id?: string;
|
giocatore_id?: string;
|
||||||
indirizzo?: string | null;
|
indirizzo?: string | null;
|
||||||
luogo_nascita?: string | null;
|
luogo_nascita?: string | null;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useGiocatoreId } from "./user-store";
|
|||||||
/**
|
/**
|
||||||
* Centro notifiche in-app (M17): pallino sull'avatar del profilo, con il numero delle non
|
* 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
|
* 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
|
* 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
|
* parallelo alla push esistente). Qui si legge e si segna come letto, con
|
||||||
* `supabaseNuoveTabelle` perché `types.ts` non include ancora questa tabella (vedi
|
* `supabaseNuoveTabelle` perché `types.ts` non include ancora questa tabella (vedi
|
||||||
|
|||||||
+40
-12
@@ -21,7 +21,6 @@ export type Profilo = {
|
|||||||
documentoRetroPath: string | null;
|
documentoRetroPath: string | null;
|
||||||
certificatoScadenza: string | null;
|
certificatoScadenza: string | null;
|
||||||
certificatoPath: string | null;
|
certificatoPath: string | null;
|
||||||
fotoPath: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RigaProfilo = {
|
export type RigaProfilo = {
|
||||||
@@ -40,11 +39,10 @@ export type RigaProfilo = {
|
|||||||
documento_retro_path: string | null;
|
documento_retro_path: string | null;
|
||||||
certificato_scadenza: string | null;
|
certificato_scadenza: string | null;
|
||||||
certificato_path: string | null;
|
certificato_path: string | null;
|
||||||
foto_path: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COLONNE_PROFILO =
|
export const COLONNE_PROFILO =
|
||||||
"giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email, documento_tipo, documento_numero, documento_rilasciato_da, documento_emissione, documento_scadenza, documento_fronte_path, documento_retro_path, certificato_scadenza, certificato_path, foto_path";
|
"giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email, documento_tipo, documento_numero, documento_rilasciato_da, documento_emissione, documento_scadenza, documento_fronte_path, documento_retro_path, certificato_scadenza, certificato_path";
|
||||||
|
|
||||||
export function profiloVuoto(giocatoreId: string): Profilo {
|
export function profiloVuoto(giocatoreId: string): Profilo {
|
||||||
return {
|
return {
|
||||||
@@ -63,7 +61,6 @@ export function profiloVuoto(giocatoreId: string): Profilo {
|
|||||||
documentoRetroPath: null,
|
documentoRetroPath: null,
|
||||||
certificatoScadenza: null,
|
certificatoScadenza: null,
|
||||||
certificatoPath: null,
|
certificatoPath: null,
|
||||||
fotoPath: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +81,6 @@ export function daRigaProfilo(r: RigaProfilo): Profilo {
|
|||||||
documentoRetroPath: r.documento_retro_path,
|
documentoRetroPath: r.documento_retro_path,
|
||||||
certificatoScadenza: r.certificato_scadenza,
|
certificatoScadenza: r.certificato_scadenza,
|
||||||
certificatoPath: r.certificato_path,
|
certificatoPath: r.certificato_path,
|
||||||
fotoPath: r.foto_path,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +107,11 @@ export function aRigaProfilo(p: Profilo): RigaProfilo {
|
|||||||
documento_retro_path: oNull(p.documentoRetroPath),
|
documento_retro_path: oNull(p.documentoRetroPath),
|
||||||
certificato_scadenza: oNull(p.certificatoScadenza),
|
certificato_scadenza: oNull(p.certificatoScadenza),
|
||||||
certificato_path: oNull(p.certificatoPath),
|
certificato_path: oNull(p.certificatoPath),
|
||||||
foto_path: oNull(p.fotoPath),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pesi delle sezioni del profilo (docs/modules/profilo-giocatore.md). */
|
/** Pesi delle sezioni del profilo (docs/modules/profilo-giocatore.md). */
|
||||||
export const PESI = { dati: 30, documento: 30, certificato: 30, foto: 10 } as const;
|
export const PESI = { dati: 34, documento: 33, certificato: 33 } as const;
|
||||||
|
|
||||||
export type Sezione = keyof typeof PESI;
|
export type Sezione = keyof typeof PESI;
|
||||||
|
|
||||||
@@ -131,7 +126,6 @@ export function sezioniComplete(p: Profilo | null | undefined): Record<Sezione,
|
|||||||
p.documentoRetroPath
|
p.documentoRetroPath
|
||||||
),
|
),
|
||||||
certificato: !!(p?.certificatoScadenza && p.certificatoPath),
|
certificato: !!(p?.certificatoScadenza && p.certificatoPath),
|
||||||
foto: !!p?.fotoPath,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +139,8 @@ export function completamento(p: Profilo | null | undefined): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* L'allenatore compila solo i dati personali ridotti (DD-034): niente indirizzo, documento,
|
* L'allenatore compila solo i dati personali ridotti (DD-034): niente indirizzo, documento
|
||||||
* certificato né foto tessera, che servono al tesseramento dei giocatori.
|
* né certificato, che servono al tesseramento dei giocatori.
|
||||||
*/
|
*/
|
||||||
export const CAMPI_ALLENATORE = ["dataNascita", "luogoNascita", "telefono", "email"] as const;
|
export const CAMPI_ALLENATORE = ["dataNascita", "luogoNascita", "telefono", "email"] as const;
|
||||||
|
|
||||||
@@ -216,9 +210,12 @@ export function etichettaGiocatore(g: GiocatoreSquadra): string {
|
|||||||
return `#${g.numero} ${nomeCompleto(g)}`;
|
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;
|
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. */
|
/** Giorni di calendario tra due date `AAAA-MM-GG` (ora locale): negativo se `scadenza` è passata. */
|
||||||
export function giorniAllaScadenza(scadenza: string, oggi: string): number {
|
export function giorniAllaScadenza(scadenza: string, oggi: string): number {
|
||||||
const a = new Date(`${scadenza}T00:00:00`);
|
const a = new Date(`${scadenza}T00:00:00`);
|
||||||
@@ -258,11 +255,13 @@ function confrontaAvvisi(a: AvvisoCertificato, b: AvvisoCertificato): number {
|
|||||||
* Certificati in scadenza o scaduti nella rosa (DD-035): calcolato al volo da
|
* 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
|
* `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).
|
* 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(
|
export function avvisiCertificati(
|
||||||
rosa: GiocatoreSquadra[],
|
rosa: GiocatoreSquadra[],
|
||||||
profili: Record<string, Profilo>,
|
profili: Record<string, Profilo>,
|
||||||
oggi: string,
|
oggi: string,
|
||||||
|
soglia: number = GIORNI_AVVISO_CERTIFICATO,
|
||||||
): { scaduti: AvvisoCertificato[]; inScadenza: AvvisoCertificato[] } {
|
): { scaduti: AvvisoCertificato[]; inScadenza: AvvisoCertificato[] } {
|
||||||
const scaduti: AvvisoCertificato[] = [];
|
const scaduti: AvvisoCertificato[] = [];
|
||||||
const inScadenza: AvvisoCertificato[] = [];
|
const inScadenza: AvvisoCertificato[] = [];
|
||||||
@@ -271,7 +270,7 @@ export function avvisiCertificati(
|
|||||||
const p = profili[g.id];
|
const p = profili[g.id];
|
||||||
if (!p?.certificatoScadenza || !p.certificatoPath) continue;
|
if (!p?.certificatoScadenza || !p.certificatoPath) continue;
|
||||||
const giorni = giorniAllaScadenza(p.certificatoScadenza, oggi);
|
const giorni = giorniAllaScadenza(p.certificatoScadenza, oggi);
|
||||||
if (giorni > GIORNI_AVVISO_CERTIFICATO) continue;
|
if (giorni > soglia) continue;
|
||||||
const avviso: AvvisoCertificato = {
|
const avviso: AvvisoCertificato = {
|
||||||
giocatoreId: g.id,
|
giocatoreId: g.id,
|
||||||
nome: g.nome,
|
nome: g.nome,
|
||||||
@@ -285,3 +284,32 @@ export function avvisiCertificati(
|
|||||||
inScadenza.sort(confrontaAvvisi);
|
inScadenza.sort(confrontaAvvisi);
|
||||||
return { scaduti, inScadenza };
|
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<string, Profilo>,
|
||||||
|
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 };
|
||||||
|
}
|
||||||
+1
-1
@@ -70,7 +70,7 @@ export function useSalvaProfilo() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SezioneFile = "documento-fronte" | "documento-retro" | "certificato" | "foto";
|
export type SezioneFile = "documento-fronte" | "documento-retro" | "certificato";
|
||||||
|
|
||||||
const MAX_BYTE = 8 * 1024 * 1024;
|
const MAX_BYTE = 8 * 1024 * 1024;
|
||||||
const TIPI_AMMESSI = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
|
const TIPI_AMMESSI = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
FileText,
|
FileText,
|
||||||
IdCard,
|
IdCard,
|
||||||
Image,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
Lock,
|
Lock,
|
||||||
Send,
|
Send,
|
||||||
@@ -487,12 +486,6 @@ function SchedaGiocatore({
|
|||||||
stato={certificato}
|
stato={certificato}
|
||||||
path={profilo?.certificatoPath ?? null}
|
path={profilo?.certificatoPath ?? null}
|
||||||
/>
|
/>
|
||||||
<Documento
|
|
||||||
icona={<Image className="h-3.5 w-3.5" />}
|
|
||||||
label="Foto"
|
|
||||||
stato={sezioni.foto ? "presente" : "assente"}
|
|
||||||
path={profilo?.fotoPath ?? null}
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
|
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const Route = createFileRoute("/calendario")({
|
|||||||
{ property: "og:title", content: "Calendario squadra — CrAPP" },
|
{ property: "og:title", content: "Calendario squadra — CrAPP" },
|
||||||
{
|
{
|
||||||
property: "og:description",
|
property: "og:description",
|
||||||
content: "Vista mensile e prossimi eventi con promemoria per la squadra.",
|
content: "Vista mensile e prossimi eventi per la squadra.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
$$;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- M29 — Via la foto tessera dal profilo (DD-044)
|
||||||
|
--
|
||||||
|
-- Elimina la colonna `profili_giocatore.foto_path`. I file `<giocatore_id>/foto.*` nel bucket
|
||||||
|
-- privato `profili-giocatore` NON si cancellano da SQL (Storage lo vieta con un trigger): vanno
|
||||||
|
-- rimossi con l'API Storage, prima di questa migration, mentre i path sono ancora leggibili.
|
||||||
|
-- Da applicare DOPO il rilascio del codice che non legge più `foto_path`: l'app precedente
|
||||||
|
-- la seleziona esplicitamente e darebbe errore.
|
||||||
|
|
||||||
|
ALTER TABLE public.profili_giocatore DROP COLUMN IF EXISTS foto_path;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- M30 — Sollecito presenze: un solo avviso automatico, 24 ore prima (DD-045)
|
||||||
|
--
|
||||||
|
-- Con M26 il job mandava il sollecito a 24, 12 e 6 ore. Ne basta uno, 24 ore prima: parte per gli
|
||||||
|
-- eventi che iniziano entro 24 ore (quindi anche per uno creato più tardi), a chi in quel momento
|
||||||
|
-- non ha risposto o ha risposto «forse». I tipi `sollecita_presenze_12h` e `_6h` restano ammessi dai
|
||||||
|
-- CHECK per le notifiche già generate, ma non ne nascono di nuove. Il pulsante manuale non cambia.
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.genera_solleciti_presenze()
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM public.genera_solleciti_presenze_fascia('sollecita_presenze_24h', interval '0', interval '24 hours');
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
REVOKE ALL ON FUNCTION public.genera_solleciti_presenze() FROM PUBLIC, anon, authenticated;
|
||||||
|
GRANT EXECUTE ON FUNCTION public.genera_solleciti_presenze() TO service_role;
|
||||||
+5
-5
@@ -15,24 +15,24 @@ INSERT INTO public.profili_giocatore
|
|||||||
(giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email,
|
(giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email,
|
||||||
documento_tipo, documento_numero, documento_rilasciato_da,
|
documento_tipo, documento_numero, documento_rilasciato_da,
|
||||||
documento_emissione, documento_scadenza, documento_fronte_path, documento_retro_path,
|
documento_emissione, documento_scadenza, documento_fronte_path, documento_retro_path,
|
||||||
certificato_scadenza, certificato_path, foto_path)
|
certificato_scadenza, certificato_path)
|
||||||
VALUES
|
VALUES
|
||||||
-- Profilo completo al 100%.
|
-- Profilo completo al 100%.
|
||||||
('g1', '1997-08-30', 'Bologna', 'Via Roma 1', '3330000001', 'g1@example.test',
|
('g1', '1997-08-30', 'Bologna', 'Via Roma 1', '3330000001', 'g1@example.test',
|
||||||
'Carta d''identità', 'CA1000001', 'Comune di Bologna',
|
'Carta d''identità', 'CA1000001', 'Comune di Bologna',
|
||||||
'2021-03-01', '2031-03-01', 'g1/documento-fronte.jpg', 'g1/documento-retro.jpg',
|
'2021-03-01', '2031-03-01', 'g1/documento-fronte.jpg', 'g1/documento-retro.jpg',
|
||||||
'2027-06-30', 'g1/certificato.pdf', 'g1/foto.jpg'),
|
'2027-06-30', 'g1/certificato.pdf'),
|
||||||
|
|
||||||
-- Certificato scaduto: in dashboard deve comparire rosso.
|
-- Certificato scaduto: in dashboard deve comparire rosso.
|
||||||
('g4', '1995-05-01', 'Bologna', 'Via Verdi 2', '3330000004', 'g4@example.test',
|
('g4', '1995-05-01', 'Bologna', 'Via Verdi 2', '3330000004', 'g4@example.test',
|
||||||
'Carta d''identità', 'CA1000004', 'Comune di Bologna',
|
'Carta d''identità', 'CA1000004', 'Comune di Bologna',
|
||||||
'2019-05-01', '2029-05-01', 'g4/documento-fronte.jpg', 'g4/documento-retro.jpg',
|
'2019-05-01', '2029-05-01', 'g4/documento-fronte.jpg', 'g4/documento-retro.jpg',
|
||||||
'2025-01-01', 'g4/certificato.pdf', 'g4/foto.jpg'),
|
'2025-01-01', 'g4/certificato.pdf'),
|
||||||
|
|
||||||
-- Profilo a metà: dati personali sì, documento no, certificato sì, foto no.
|
-- Profilo a metà: dati personali sì, documento no, certificato sì.
|
||||||
('g2', '1996-12-07', 'Modena', 'Via Bianchi 3', '3330000002', 'g2@example.test',
|
('g2', '1996-12-07', 'Modena', 'Via Bianchi 3', '3330000002', 'g2@example.test',
|
||||||
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
||||||
'2027-09-15', 'g2/certificato.pdf', NULL)
|
'2027-09-15', 'g2/certificato.pdf')
|
||||||
ON CONFLICT (giocatore_id) DO NOTHING;
|
ON CONFLICT (giocatore_id) DO NOTHING;
|
||||||
|
|
||||||
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
|
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
|
||||||
|
|||||||
+2
-2
@@ -25,9 +25,9 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
|
|||||||
## Struttura
|
## Struttura
|
||||||
|
|
||||||
| Cartella | Cosa verifica | Serve rete? |
|
| Cartella | Cosa verifica | Serve rete? |
|
||||||
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
|
||||||
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No |
|
| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, 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ì |
|
| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26-M28 e M30, DD-040, DD-042, DD-043 e DD-045: palloni un solo avviso a 3 ore, sollecito uno solo a 24 ore, nessun promemoria evento) la colonna `foto_path` eliminata (`foto-tessera-rimossa`, M29, DD-044) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì |
|
||||||
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
|
||||||
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Via la foto tessera dal profilo (M29, DD-044): `bun test/integration/foto-tessera-rimossa.test.ts`.
|
||||||
|
*
|
||||||
|
* Gira solo sullo stack locale (`npx supabase start`): verifica che la colonna `foto_path` non ci
|
||||||
|
* sia più e che le colonne lette dall'app (`COLONNE_PROFILO`) esistano tutte. Non guarda il
|
||||||
|
* database di `.env`, che può essere ancora quello di produzione prima di applicare la M29.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { COLONNE_PROFILO } from "@/lib/profili-core";
|
||||||
|
import { statoLocale } from "../helpers/locale";
|
||||||
|
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||||
|
|
||||||
|
const locale = statoLocale();
|
||||||
|
|
||||||
|
if (!locale) {
|
||||||
|
salta("foto tessera rimossa", "stack locale non attivo (npx supabase start)");
|
||||||
|
riepilogo("foto-tessera-rimossa");
|
||||||
|
} else {
|
||||||
|
const leggi = (colonne: string) =>
|
||||||
|
fetch(`${locale.url}/rest/v1/profili_giocatore?select=${encodeURIComponent(colonne)}&limit=1`, {
|
||||||
|
headers: { apikey: locale.servizio, Authorization: `Bearer ${locale.servizio}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("la colonna foto_path non esiste più", async () => {
|
||||||
|
const res = await leggi("foto_path");
|
||||||
|
assert.equal(res.status, 400, "PostgREST rifiuta una colonna che non c'è");
|
||||||
|
assert.match(await res.text(), /foto_path/);
|
||||||
|
});
|
||||||
|
|
||||||
|
await prova("le colonne lette dall'app esistono tutte", async () => {
|
||||||
|
assert.ok(!COLONNE_PROFILO.includes("foto"), "COLONNE_PROFILO non nomina la foto");
|
||||||
|
const res = await leggi(COLONNE_PROFILO);
|
||||||
|
assert.equal(res.status, 200, await res.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
riepilogo("foto-tessera-rimossa");
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
* Esegue i job veri del database (`genera_avvisi_palloni`, `genera_solleciti_presenze`) su eventi
|
* Esegue i job veri del database (`genera_avvisi_palloni`, `genera_solleciti_presenze`) su eventi
|
||||||
* creati con orari relativi a «adesso» e verifica:
|
* creati con orari relativi a «adesso» e verifica:
|
||||||
*
|
*
|
||||||
* - le finestre contigue e senza sovrapposizione (palloni 12·6·3 ore, solleciti 24·12·6 ore);
|
* - 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: un solo avviso nelle 24 ore prima, DD-045);
|
||||||
* - i destinatari (incaricato e turno precedente; convocati senza risposta o con «forse»; mai
|
* - i destinatari (incaricato e turno precedente; convocati senza risposta o con «forse»; mai
|
||||||
* l'allenatore né chi non è più attivo);
|
* l'allenatore né chi non è più attivo);
|
||||||
* - una sola generazione per giocatore, evento e tipo, anche se la notifica viene eliminata;
|
* - una sola generazione per giocatore, evento e tipo, anche se la notifica viene eliminata;
|
||||||
@@ -46,12 +47,14 @@ if (!locale) {
|
|||||||
const INATTIVO = "g9";
|
const INATTIVO = "g9";
|
||||||
const ALLENATORE = "g10";
|
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 PASSATO = `${PREFISSO}-passato`;
|
||||||
const PREC = `${PREFISSO}-prec`;
|
const PREC = `${PREFISSO}-prec`;
|
||||||
const MAIN = `${PREFISSO}-main`;
|
const MAIN = `${PREFISSO}-main`;
|
||||||
const EV6 = `${PREFISSO}-ev6`;
|
const SECONDO = `${PREFISSO}-secondo`;
|
||||||
const EV12 = `${PREFISSO}-ev12`;
|
const TERZO = `${PREFISSO}-terzo`;
|
||||||
|
const FUORI4 = `${PREFISSO}-fuori4`;
|
||||||
|
const FUORI9 = `${PREFISSO}-fuori9`;
|
||||||
const LONTANO = `${PREFISSO}-lontano`;
|
const LONTANO = `${PREFISSO}-lontano`;
|
||||||
const GENERICO = `${PREFISSO}-generico`;
|
const GENERICO = `${PREFISSO}-generico`;
|
||||||
const COPPIA_A = `${PREFISSO}-coppia-a`;
|
const COPPIA_A = `${PREFISSO}-coppia-a`;
|
||||||
@@ -68,8 +71,10 @@ if (!locale) {
|
|||||||
PASSATO,
|
PASSATO,
|
||||||
PREC,
|
PREC,
|
||||||
MAIN,
|
MAIN,
|
||||||
EV6,
|
SECONDO,
|
||||||
EV12,
|
TERZO,
|
||||||
|
FUORI4,
|
||||||
|
FUORI9,
|
||||||
LONTANO,
|
LONTANO,
|
||||||
GENERICO,
|
GENERICO,
|
||||||
COPPIA_A,
|
COPPIA_A,
|
||||||
@@ -266,36 +271,51 @@ if (!locale) {
|
|||||||
);
|
);
|
||||||
await scrivi(`giocatori_squadra?id=eq.${INATTIVO}`, "PATCH", { attivo: false }, tokenAdmin);
|
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(PASSATO, "partita", tra(-60));
|
||||||
await creaEvento(PREC, "allenamento", tra(-30));
|
await creaEvento(PREC, "allenamento", tra(-30));
|
||||||
await creaEvento(MAIN, "allenamento", tra(2.5));
|
await creaEvento(MAIN, "allenamento", tra(0.8));
|
||||||
await creaEvento(EV6, "partita", tra(4.5));
|
await creaEvento(SECONDO, "partita", tra(1.5));
|
||||||
await creaEvento(EV12, "allenamento", tra(9));
|
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(LONTANO, "allenamento", tra(40));
|
||||||
await creaEvento(GENERICO, "evento", tra(2.2));
|
await creaEvento(GENERICO, "evento", tra(1.9));
|
||||||
await assegna(PREC, G5);
|
await assegna(PREC, G5);
|
||||||
await assegna(MAIN, G4);
|
await assegna(MAIN, G4);
|
||||||
await assegna(EV6, G7);
|
await assegna(SECONDO, G7);
|
||||||
await assegna(EV12, G8);
|
await assegna(TERZO, G8);
|
||||||
|
await assegna(FUORI4, G7);
|
||||||
|
await assegna(FUORI9, G8);
|
||||||
await assegna(GENERICO, G4); // extra-campo: il turno non conta
|
await assegna(GENERICO, G4); // extra-campo: il turno non conta
|
||||||
|
|
||||||
// --- Turno palloni: finestre e destinatari ---------------------------------------------
|
// --- Turno palloni: finestre e destinatari ---------------------------------------------
|
||||||
await prova("ogni evento riceve l'avviso della sua fascia e di nessun'altra", async () => {
|
await prova(
|
||||||
|
"un solo avviso, 3 ore prima, a chi porta i palloni e a chi li prende",
|
||||||
|
async () => {
|
||||||
await esegui("genera_avvisi_palloni");
|
await esegui("genera_avvisi_palloni");
|
||||||
assert.deepEqual(await destinatari(MAIN, "turno_palloni_3h"), [G4, G5]);
|
assert.deepEqual(await destinatari(MAIN, "turno_palloni_3h"), [G4, G5]);
|
||||||
assert.deepEqual(await destinatari(EV6, "turno_palloni_6h"), [G4, G7]);
|
assert.deepEqual(await destinatari(SECONDO, "turno_palloni_3h"), [G4, G7]);
|
||||||
assert.deepEqual(await destinatari(EV12, "turno_palloni_12h"), [G7, G8]);
|
assert.deepEqual(await destinatari(TERZO, "turno_palloni_3h"), [G7, G8]);
|
||||||
for (const [evento, altri] of [
|
for (const evento of [MAIN, SECONDO, TERZO]) {
|
||||||
[MAIN, ["turno_palloni_6h", "turno_palloni_12h"]],
|
for (const tipo of ["turno_palloni_12h", "turno_palloni_6h"]) {
|
||||||
[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}`);
|
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(
|
await prova(
|
||||||
"nessun avviso per eventi lontani, passati, extra-campo o senza incaricato",
|
"nessun avviso per eventi lontani, passati, extra-campo o senza incaricato",
|
||||||
@@ -314,8 +334,8 @@ if (!locale) {
|
|||||||
const turni = await turniDelDatabase();
|
const turni = await turniDelDatabase();
|
||||||
for (const [evento, tipo] of [
|
for (const [evento, tipo] of [
|
||||||
[MAIN, "turno_palloni_3h"],
|
[MAIN, "turno_palloni_3h"],
|
||||||
[EV6, "turno_palloni_6h"],
|
[SECONDO, "turno_palloni_3h"],
|
||||||
[EV12, "turno_palloni_12h"],
|
[TERZO, "turno_palloni_3h"],
|
||||||
] as const) {
|
] as const) {
|
||||||
const attesi = avvisiPalloniEvento(turni, eventi, evento);
|
const attesi = avvisiPalloniEvento(turni, eventi, evento);
|
||||||
assert.equal(attesi.length, 2, evento);
|
assert.equal(attesi.length, 2, evento);
|
||||||
@@ -330,7 +350,7 @@ if (!locale) {
|
|||||||
const [incarico] = (await notifiche(MAIN, "turno_palloni_3h")).filter(
|
const [incarico] = (await notifiche(MAIN, "turno_palloni_3h")).filter(
|
||||||
(r) => r.giocatore_id === G4,
|
(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)!;
|
const main = eventi.find((e) => e.id === MAIN)!;
|
||||||
assert.equal(
|
assert.equal(
|
||||||
incarico?.corpo,
|
incarico?.corpo,
|
||||||
@@ -338,7 +358,7 @@ if (!locale) {
|
|||||||
`Evento: ${PREFISSO} main`,
|
`Evento: ${PREFISSO} main`,
|
||||||
`Data: ${gg(main.data)}, ore ${main.ora}`,
|
`Data: ${gg(main.data)}, ore ${main.ora}`,
|
||||||
"Incarico: custodia dei palloni al termine dell'evento",
|
"Incarico: custodia dei palloni al termine dell'evento",
|
||||||
`Riconsegna: ${gg(eventiEv6.data)}`,
|
`Riconsegna: ${gg(eventoSecondo.data)}`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"la riconsegna è la data dell'evento successivo",
|
"la riconsegna è la data dell'evento successivo",
|
||||||
);
|
);
|
||||||
@@ -349,8 +369,8 @@ if (!locale) {
|
|||||||
assert.equal((await registro(MAIN, "turno_palloni_3h")).length, 2);
|
assert.equal((await registro(MAIN, "turno_palloni_3h")).length, 2);
|
||||||
const righe = [
|
const righe = [
|
||||||
...(await notifiche(MAIN, "turno_palloni_3h")),
|
...(await notifiche(MAIN, "turno_palloni_3h")),
|
||||||
...(await notifiche(EV6, "turno_palloni_6h")),
|
...(await notifiche(SECONDO, "turno_palloni_3h")),
|
||||||
...(await notifiche(EV12, "turno_palloni_12h")),
|
...(await notifiche(TERZO, "turno_palloni_3h")),
|
||||||
];
|
];
|
||||||
const ids = righe.map((r) => r.id);
|
const ids = righe.map((r) => r.id);
|
||||||
assert.equal(ids.length, 6);
|
assert.equal(ids.length, 6);
|
||||||
@@ -385,13 +405,13 @@ if (!locale) {
|
|||||||
"ora non valida: escluso, senza errori",
|
"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");
|
await esegui("genera_avvisi_palloni");
|
||||||
assert.ok(
|
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",
|
"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",
|
"turno tolto: chi era stato avvisato, incaricato o riconsegna, viene revocato",
|
||||||
async () => {
|
async () => {
|
||||||
// La notifica dell'avviso può essere stata eliminata: conta il registro.
|
// 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,
|
(r) => r.giocatore_id === G7,
|
||||||
);
|
);
|
||||||
await scrivi(`notifiche_utente?id=eq.${avvisoG7!.id}`, "DELETE");
|
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");
|
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(G7), "g7 aveva eliminato l'avviso: conta il registro");
|
||||||
assert.ok(!revocati.includes(G8), "g8 non c'entra con questo evento");
|
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.
|
// Senza incaricato non parte nulla di nuovo: restano g4 e g6, avvisati prima come
|
||||||
assert.equal((await notifiche(EV6, "turno_palloni_3h")).length, 0);
|
// «riconsegna» quando il turno di MAIN è passato a g6 e poi tornato a g4.
|
||||||
await assegna(EV6, G7);
|
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.
|
// Solo i due eventi della coppia cadono nella fascia: 8 giorni ± un giorno.
|
||||||
await esegui("genera_avvisi_palloni_fascia", {
|
await esegui("genera_avvisi_palloni_fascia", {
|
||||||
p_tipo: "turno_palloni_12h",
|
p_tipo: "turno_palloni_3h",
|
||||||
p_da: `${24 * 7} hours`,
|
p_da: `${24 * 7} hours`,
|
||||||
p_a: `${24 * 9} hours`,
|
p_a: `${24 * 9} hours`,
|
||||||
});
|
});
|
||||||
for (const evento of [COPPIA_A, COPPIA_B]) {
|
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);
|
const attesi = avvisiPalloniEvento(turni, eventi, evento);
|
||||||
assert.equal(righe.length, attesi.length, evento);
|
assert.equal(righe.length, attesi.length, evento);
|
||||||
for (const a of attesi) {
|
for (const a of attesi) {
|
||||||
@@ -545,7 +566,7 @@ if (!locale) {
|
|||||||
.sort();
|
.sort();
|
||||||
|
|
||||||
await prova(
|
await prova(
|
||||||
"ogni evento riceve il sollecito della sua fascia, a chi deve rispondere",
|
"un solo sollecito, entro 24 ore dall'inizio, a chi deve rispondere (DD-045)",
|
||||||
async () => {
|
async () => {
|
||||||
await esegui("genera_solleciti_presenze");
|
await esegui("genera_solleciti_presenze");
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -555,21 +576,18 @@ if (!locale) {
|
|||||||
);
|
);
|
||||||
const senzaRisposta = (await tuttiGiocatoriAttivi()).filter((g) => g !== G4);
|
const senzaRisposta = (await tuttiGiocatoriAttivi()).filter((g) => g !== G4);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await destinatari(S12, "sollecita_presenze_12h"),
|
await destinatari(S12, "sollecita_presenze_24h"),
|
||||||
senzaRisposta,
|
senzaRisposta,
|
||||||
"convocati vuoto: tutta la rosa attiva, meno chi ha risposto; g5 («forse») resta",
|
"convocati vuoto: tutta la rosa attiva, meno chi ha risposto; g5 («forse») resta",
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await destinatari(S6, "sollecita_presenze_6h"),
|
await destinatari(S6, "sollecita_presenze_24h"),
|
||||||
(await tuttiGiocatoriAttivi()).filter((g) => g !== G4),
|
(await tuttiGiocatoriAttivi()).filter((g) => g !== G4),
|
||||||
|
"anche a 4 ore dall'inizio: il sollecito è uno solo, non uno per fascia",
|
||||||
);
|
);
|
||||||
assert.equal((await notifiche(S_FUORI)).length, 0, "tra 30 ore: fuori da ogni fascia");
|
assert.equal((await notifiche(S_FUORI)).length, 0, "tra 30 ore: non ancora");
|
||||||
for (const [evento, altri] of [
|
for (const evento of [S24, S12, S6]) {
|
||||||
[S24, ["sollecita_presenze_12h", "sollecita_presenze_6h"]],
|
for (const tipo of ["sollecita_presenze_12h", "sollecita_presenze_6h"]) {
|
||||||
[S12, ["sollecita_presenze_24h", "sollecita_presenze_6h"]],
|
|
||||||
[S6, ["sollecita_presenze_24h", "sollecita_presenze_12h"]],
|
|
||||||
] as const) {
|
|
||||||
for (const tipo of altri) {
|
|
||||||
assert.equal((await notifiche(evento, tipo)).length, 0, `${evento} non ha ${tipo}`);
|
assert.equal((await notifiche(evento, tipo)).length, 0, `${evento} non ha ${tipo}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -593,8 +611,8 @@ if (!locale) {
|
|||||||
}>("giocatori_squadra?select=id,attivo,tipo");
|
}>("giocatori_squadra?select=id,attivo,tipo");
|
||||||
for (const [id, tipo] of [
|
for (const [id, tipo] of [
|
||||||
[S24, "sollecita_presenze_24h"],
|
[S24, "sollecita_presenze_24h"],
|
||||||
[S12, "sollecita_presenze_12h"],
|
[S12, "sollecita_presenze_24h"],
|
||||||
[S6, "sollecita_presenze_6h"],
|
[S6, "sollecita_presenze_24h"],
|
||||||
] as const) {
|
] as const) {
|
||||||
const evento = eventi.find((e) => e.id === id)!;
|
const evento = eventi.find((e) => e.id === id)!;
|
||||||
const risposte = await leggi<{ giocatore_id: string; stato: string }>(
|
const risposte = await leggi<{ giocatore_id: string; stato: string }>(
|
||||||
@@ -634,41 +652,33 @@ if (!locale) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await prova(
|
await prova(
|
||||||
"chi risponde smette di riceverli, chi resta su «forse» li riceve tutti e tre",
|
"chi resta su «forse» riceve il sollecito una sola volta, non a ogni fascia",
|
||||||
async () => {
|
async () => {
|
||||||
await creaEvento(S_FORSE, "evento", tra(18), { convocati: [G4, G5, G8] });
|
await creaEvento(S_FORSE, "evento", tra(18), { convocati: [G4, G5, G8] });
|
||||||
await rispondi(S_FORSE, G5, "forse");
|
await rispondi(S_FORSE, G5, "forse");
|
||||||
await esegui("genera_solleciti_presenze");
|
await esegui("genera_solleciti_presenze");
|
||||||
assert.deepEqual(await destinatari(S_FORSE, "sollecita_presenze_24h"), [G4, G5, G8]);
|
assert.deepEqual(await destinatari(S_FORSE, "sollecita_presenze_24h"), [G4, G5, G8]);
|
||||||
|
|
||||||
await rispondi(S_FORSE, G4, "presente"); // risponde dopo il primo sollecito
|
await rispondi(S_FORSE, G4, "presente");
|
||||||
await spostaEvento(S_FORSE, 9); // entra nella fascia delle 12 ore
|
await spostaEvento(S_FORSE, 9);
|
||||||
await esegui("genera_solleciti_presenze");
|
await esegui("genera_solleciti_presenze");
|
||||||
assert.deepEqual(
|
|
||||||
await destinatari(S_FORSE, "sollecita_presenze_12h"),
|
|
||||||
[G5, G8],
|
|
||||||
"g4 ha risposto: niente secondo sollecito; «forse» e nessuna risposta sì",
|
|
||||||
);
|
|
||||||
|
|
||||||
await rispondi(S_FORSE, G8, "assente");
|
|
||||||
await spostaEvento(S_FORSE, 4);
|
await spostaEvento(S_FORSE, 4);
|
||||||
await esegui("genera_solleciti_presenze");
|
await esegui("genera_solleciti_presenze");
|
||||||
assert.deepEqual(await destinatari(S_FORSE, "sollecita_presenze_6h"), [G5]);
|
assert.deepEqual(
|
||||||
assert.equal(
|
(await notifiche(S_FORSE)).map((r) => r.tipo),
|
||||||
(await notifiche(S_FORSE)).filter((r) => r.giocatore_id === G5).length,
|
["sollecita_presenze_24h", "sollecita_presenze_24h", "sollecita_presenze_24h"],
|
||||||
3,
|
"nessun secondo sollecito avvicinandosi all'evento, nemmeno a g5 («forse»)",
|
||||||
"g5, sempre «forse»: 24, 12 e 6 ore",
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await prova("un evento creato tardi riceve solo il sollecito della sua fascia", async () => {
|
await prova("un evento creato tardi riceve comunque un solo sollecito", async () => {
|
||||||
await creaEvento(S_TARDI, "evento", tra(5), { convocati: [G4] });
|
await creaEvento(S_TARDI, "evento", tra(5), { convocati: [G4] });
|
||||||
await esegui("genera_solleciti_presenze");
|
await esegui("genera_solleciti_presenze");
|
||||||
|
await esegui("genera_solleciti_presenze");
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
(await notifiche(S_TARDI)).map((r) => r.tipo),
|
(await notifiche(S_TARDI)).map((r) => r.tipo),
|
||||||
["sollecita_presenze_6h"],
|
["sollecita_presenze_24h"],
|
||||||
"non tre solleciti insieme",
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -774,6 +784,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 () => {
|
await prova("cancellare l'evento toglie registro e notifiche collegate", async () => {
|
||||||
assert.ok((await registro(MAIN)).length > 0);
|
assert.ok((await registro(MAIN)).length > 0);
|
||||||
await scrivi(`eventi_app?id=eq.${MAIN}`, "DELETE");
|
await scrivi(`eventi_app?id=eq.${MAIN}`, "DELETE");
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import assert from "node:assert/strict";
|
|||||||
import {
|
import {
|
||||||
aRigaProfilo,
|
aRigaProfilo,
|
||||||
avvisiCertificati,
|
avvisiCertificati,
|
||||||
|
avvisiCertificatiUtente,
|
||||||
|
GIORNI_AVVISO_CERTIFICATO_GIOCATORE,
|
||||||
completamento,
|
completamento,
|
||||||
completamentoAllenatore,
|
completamentoAllenatore,
|
||||||
csvTesseramento,
|
csvTesseramento,
|
||||||
|
PESI,
|
||||||
daRigaProfilo,
|
daRigaProfilo,
|
||||||
formatDataBreve,
|
formatDataBreve,
|
||||||
giorniAllaScadenza,
|
giorniAllaScadenza,
|
||||||
@@ -39,7 +42,6 @@ const vuoto: Profilo = {
|
|||||||
documentoRetroPath: null,
|
documentoRetroPath: null,
|
||||||
certificatoScadenza: null,
|
certificatoScadenza: null,
|
||||||
certificatoPath: null,
|
certificatoPath: null,
|
||||||
fotoPath: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const completo: Profilo = {
|
const completo: Profilo = {
|
||||||
@@ -58,18 +60,22 @@ const completo: Profilo = {
|
|||||||
documentoRetroPath: "g1/documento-retro.jpg",
|
documentoRetroPath: "g1/documento-retro.jpg",
|
||||||
certificatoScadenza: "2027-06-30",
|
certificatoScadenza: "2027-06-30",
|
||||||
certificatoPath: "g1/certificato.pdf",
|
certificatoPath: "g1/certificato.pdf",
|
||||||
fotoPath: "g1/foto.jpg",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- completamento -----------------------------------------------------------
|
// --- completamento -----------------------------------------------------------
|
||||||
assert.equal(completamento(null), 0, "profilo inesistente = 0%");
|
assert.equal(completamento(null), 0, "profilo inesistente = 0%");
|
||||||
assert.equal(completamento(vuoto), 0);
|
assert.equal(completamento(vuoto), 0);
|
||||||
assert.equal(completamento(completo), 100, "tutte le sezioni piene = 100%");
|
assert.equal(completamento(completo), 100, "tutte le sezioni piene = 100%");
|
||||||
assert.equal(completamento({ ...completo, fotoPath: null }), 90, "la foto pesa 10");
|
assert.equal(
|
||||||
assert.equal(completamento({ ...completo, certificatoPath: null }), 70, "il certificato pesa 30");
|
Object.values(PESI).reduce((somma, peso) => somma + peso, 0),
|
||||||
|
100,
|
||||||
|
"senza foto tessera (DD-044) i pesi di dati, documento e certificato fanno 100",
|
||||||
|
);
|
||||||
|
assert.deepEqual(Object.keys(sezioniComplete(completo)), ["dati", "documento", "certificato"]);
|
||||||
|
assert.equal(completamento({ ...completo, certificatoPath: null }), 67, "il certificato pesa 33");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
completamento({ ...completo, email: null }),
|
completamento({ ...completo, email: null }),
|
||||||
70,
|
66,
|
||||||
"i dati personali sono completi solo tutti insieme",
|
"i dati personali sono completi solo tutti insieme",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -214,7 +220,7 @@ assert.equal(
|
|||||||
email: "a@b.it",
|
email: "a@b.it",
|
||||||
}),
|
}),
|
||||||
100,
|
100,
|
||||||
"bastano i quattro dati personali: niente indirizzo, documento, certificato, foto",
|
"bastano i quattro dati personali: niente indirizzo, documento, certificato",
|
||||||
);
|
);
|
||||||
assert.equal(completamentoAllenatore({ ...vuoto, telefono: " " }), 0, "spazi soli non contano");
|
assert.equal(completamentoAllenatore({ ...vuoto, telefono: " " }), 0, "spazi soli non contano");
|
||||||
|
|
||||||
@@ -333,4 +339,78 @@ assert.deepEqual(
|
|||||||
"il più vecchio prima, poi alfabetico a parità di data (Alfa prima di Zeta)",
|
"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<string, Profilo> = {
|
||||||
|
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<string, Profilo> = {
|
||||||
|
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");
|
console.log("profili-core: ok");
|
||||||
@@ -14,11 +14,11 @@ await assert.rejects(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// --- file troppo grande: rifiutato anche con un formato valido ---------------
|
// --- file troppo grande: rifiutato anche con un formato valido ---------------
|
||||||
const troppoGrande = new File([new Uint8Array(8 * 1024 * 1024 + 1)], "foto.jpg", {
|
const troppoGrande = new File([new Uint8Array(8 * 1024 * 1024 + 1)], "certificato.jpg", {
|
||||||
type: "image/jpeg",
|
type: "image/jpeg",
|
||||||
});
|
});
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
() => caricaFile("g1", "foto", troppoGrande),
|
() => caricaFile("g1", "certificato", troppoGrande),
|
||||||
/File troppo grande: massimo 8 MB\./,
|
/File troppo grande: massimo 8 MB\./,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/** Check dei campi del profilo: `bun test/unit/profilo-campi.test.ts` (DD-044: niente foto tessera). */
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { createElement } from "react";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { CampiProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
||||||
|
import { profiloVuoto, sezioniComplete } from "@/lib/profili-core";
|
||||||
|
|
||||||
|
const corrente = profiloVuoto("g1");
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
createElement(CampiProfilo, {
|
||||||
|
corrente,
|
||||||
|
aggiorna: () => {},
|
||||||
|
sezioni: sezioniComplete(corrente),
|
||||||
|
fileDocumento: createElement("p", null, "slot-documento"),
|
||||||
|
fileCertificato: createElement("p", null, "slot-certificato"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- le sezioni rimaste ci sono ----------------------------------------------
|
||||||
|
assert.ok(html.includes("Dati personali"));
|
||||||
|
assert.ok(html.includes("slot-documento"), "lo slot del documento è mostrato");
|
||||||
|
assert.ok(html.includes("slot-certificato"), "lo slot del certificato è mostrato");
|
||||||
|
|
||||||
|
// --- la foto tessera non c'è più, né come titolo né come campo ----------------
|
||||||
|
assert.ok(!/foto tessera/i.test(html), "nessun riferimento alla foto tessera");
|
||||||
|
assert.ok(!html.includes("fotoPath"));
|
||||||
|
|
||||||
|
console.log("profilo-campi: ok");
|
||||||
Reference in new issue
Block a user