diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e74a0e4..d7b7053 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,12 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +### Modificato + +- **Avviso certificati: giocatore a 30 giorni** — il giocatore vede l'avviso giallo sul proprio + certificato medico da un mese prima della scadenza (prima 7 giorni). Per gli admin non cambia + nulla: 7 giorni prima in giallo, nero dopo la scadenza (DD-041). + ## [1.2.1] - 2026-09-30 Turno palloni e sollecito presenze partono anche da soli, a orari fissi prima dell'evento, e i testi di diff --git a/docs/decisions/DD-035.md b/docs/decisions/DD-035.md index 31c474c..64875cd 100644 --- a/docs/decisions/DD-035.md +++ b/docs/decisions/DD-035.md @@ -1,7 +1,7 @@ # DD-035 — Avviso certificati: calcolato in Home, visibile solo al titolare e agli admin **Data:** 24 settembre 2026 -**Stato:** Accettata (implementata, versione 1.1.0) +**Stato:** Accettata (implementata, versione 1.1.0). La soglia del giocatore è stata alzata a 30 giorni da [DD-041](DD-041.md); quella dello staff resta 7. **Contesto** Il certificato medico ha una data di scadenza (`profili_giocatore.certificato_scadenza`), ma @@ -49,7 +49,7 @@ certificato viene rinnovato. Specifica completa in - Il rinnovo non richiede nessuna azione di chiusura: cambiata la data, l'avviso si aggiorna da solo. - Chi non apre l'app non viene avvisato: l'avviso raggiunge solo chi entra in Home. -- Un admin che è anche giocatore vede solo l'avviso dello staff, dove compare il suo nome. +- ~~Un admin che è anche giocatore vede solo l'avviso dello staff~~ → superato da DD-041: vede anche il proprio avviso personale. **Riesame** Se gli avvisi in Home non bastano (certificati che scadono comunque senza essere rinnovati), diff --git a/docs/decisions/DD-041.md b/docs/decisions/DD-041.md new file mode 100644 index 0000000..688ea4f --- /dev/null +++ b/docs/decisions/DD-041.md @@ -0,0 +1,39 @@ +# DD-041 — Avviso certificati: il giocatore è avvisato un mese prima dello staff + +**Data:** 1 ottobre 2026 +**Stato:** Accettata (modifica la soglia di [DD-035](DD-035.md) per il solo giocatore) + +**Contesto** +Con DD-035 giocatore e admin vedono l'avviso negli stessi 7 giorni prima della scadenza. Per +il giocatore sono pochi: prenotare una visita di idoneità richiede più tempo (era il rischio +indicato nel Riesame di DD-035). All'admin invece un avviso così anticipato riempirebbe la +Home di nomi che non può ancora sollecitare con urgenza. + +**Decisione** + +1. Il **giocatore** vede l'avviso giallo da **30 giorni** prima della scadenza + (`GIORNI_AVVISO_CERTIFICATO_GIOCATORE`). +2. L'**admin** resta a **7 giorni** prima (`GIORNI_AVVISO_CERTIFICATO`), invariato. +3. Dopo la scadenza l'avviso è **nero** per entrambi, invariato. +4. L'**admin che è anche giocatore** vede il proprio avviso **personale** (30 giorni, + cliccabile) oltre a quello dello staff, dal quale il suo nome è tolto per non duplicarlo. +5. Nessun altro aspetto di DD-035 cambia: calcolo al volo, niente push, stessi destinatari. + +Esempio: scadenza il 5 ottobre 2026 → dal 5 settembre l'avviso giallo compare solo al +giocatore; dal 28 settembre compare anche agli admin; dal 6 ottobre è nero per entrambi. + +**Alternative scartate** + +- «Un mese» come mese di calendario → la soglia varierebbe tra 28 e 31 giorni; 30 giorni fissi + sono prevedibili e coincidono con l'esempio della richiesta. +- Soglia unica a 30 giorni anche per l'admin → elenco dello staff pieno con settimane di + anticipo, senza nulla da fare per l'admin che non può caricare il file (DD-017). + +**Conseguenze** + +- Il giocatore vede il conto alla rovescia «scade tra N giorni» con N fino a 30. +- `avvisiCertificati()` riceve la soglia come parametro (default 7, cioè lo staff). + +**Riesame** +Se 30 giorni risultano ancora pochi o troppi, cambiare la costante; se serve una soglia +regolabile dall'admin, vedi il Riesame di DD-035. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index db9f954..de708db 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -46,46 +46,47 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand **Accettate** -| ID | Titolo | -| ------------------- | ------------------------------------- | -| [DD-001](DD-001.md) | Indipendenza da Lovable | -| [DD-002](DD-002.md) | Sviluppo document-first | -| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | -| [DD-005](DD-005.md) | Mobile-first | -| [DD-006](DD-006.md) | AI solo se utile | -| [DD-007](DD-007.md) | Badge calcolati, non in DB | -| [DD-008](DD-008.md) | Gamification equa tra ruoli | -| [DD-009](DD-009.md) | CSI manuale, poi integrazione API | -| [DD-010](DD-010.md) | Niente storico certificati | -| [DD-011](DD-011.md) | Auth reale prima del profilo | -| [DD-012](DD-012.md) | Rimandare la migrazione ID | -| [DD-013](DD-013.md) | Portabilità dello stack | -| [DD-015](DD-015.md) | Rosa da hardcoded a DB | -| [DD-016](DD-016.md) | Schema dati Profilo Giocatore | -| [DD-017](DD-017.md) | L'admin scrive al posto del giocatore | -| [DD-018](DD-018.md) | Collegamento automatico per email | -| [DD-019](DD-019.md) | Il branch lo decide l'utente | -| [DD-020](DD-020.md) | Test obbligatori e verdi | -| [DD-021](DD-021.md) | Molle interrompibili con motion | -| [DD-022](DD-022.md) | App solo chiara | -| [DD-023](DD-023.md) | Scritture limitate per ruolo | -| [DD-024](DD-024.md) | Route di notifica autenticate | -| [DD-025](DD-025.md) | Promemoria palloni manuale | -| [DD-026](DD-026.md) | Payload push cifrato | -| [DD-027](DD-027.md) | Voto limitato ai convocati | -| [DD-028](DD-028.md) | Soglia minima Media voto e MVP | -| [DD-029](DD-029.md) | Pulizia a cascata evento cancellato | -| [DD-030](DD-030.md) | Centro notifiche in-app | -| [DD-031](DD-031.md) | Nascita pubblica sincronizzata | -| [DD-032](DD-032.md) | Stagione CSI dalle date delle gare | -| [DD-033](DD-033.md) | Gestione eventi solo da calendario | -| [DD-034](DD-034.md) | Ruolo allenatore | -| [DD-035](DD-035.md) | Avviso certificati in Home | -| [DD-036](DD-036.md) | Notifiche email via Gmail | -| [DD-037](DD-037.md) | Promemoria evento senza ritorni | -| [DD-038](DD-038.md) | Tre canali per ogni notifica | -| [DD-039](DD-039.md) | Niente promemoria all'allenatore | -| [DD-040](DD-040.md) | Palloni e solleciti automatici | +| ID | Titolo | +| ------------------- | ----------------------------------------- | +| [DD-001](DD-001.md) | Indipendenza da Lovable | +| [DD-002](DD-002.md) | Sviluppo document-first | +| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | +| [DD-005](DD-005.md) | Mobile-first | +| [DD-006](DD-006.md) | AI solo se utile | +| [DD-007](DD-007.md) | Badge calcolati, non in DB | +| [DD-008](DD-008.md) | Gamification equa tra ruoli | +| [DD-009](DD-009.md) | CSI manuale, poi integrazione API | +| [DD-010](DD-010.md) | Niente storico certificati | +| [DD-011](DD-011.md) | Auth reale prima del profilo | +| [DD-012](DD-012.md) | Rimandare la migrazione ID | +| [DD-013](DD-013.md) | Portabilità dello stack | +| [DD-015](DD-015.md) | Rosa da hardcoded a DB | +| [DD-016](DD-016.md) | Schema dati Profilo Giocatore | +| [DD-017](DD-017.md) | L'admin scrive al posto del giocatore | +| [DD-018](DD-018.md) | Collegamento automatico per email | +| [DD-019](DD-019.md) | Il branch lo decide l'utente | +| [DD-020](DD-020.md) | Test obbligatori e verdi | +| [DD-021](DD-021.md) | Molle interrompibili con motion | +| [DD-022](DD-022.md) | App solo chiara | +| [DD-023](DD-023.md) | Scritture limitate per ruolo | +| [DD-024](DD-024.md) | Route di notifica autenticate | +| [DD-025](DD-025.md) | Promemoria palloni manuale | +| [DD-026](DD-026.md) | Payload push cifrato | +| [DD-027](DD-027.md) | Voto limitato ai convocati | +| [DD-028](DD-028.md) | Soglia minima Media voto e MVP | +| [DD-029](DD-029.md) | Pulizia a cascata evento cancellato | +| [DD-030](DD-030.md) | Centro notifiche in-app | +| [DD-031](DD-031.md) | Nascita pubblica sincronizzata | +| [DD-032](DD-032.md) | Stagione CSI dalle date delle gare | +| [DD-033](DD-033.md) | Gestione eventi solo da calendario | +| [DD-034](DD-034.md) | Ruolo allenatore | +| [DD-035](DD-035.md) | Avviso certificati in Home | +| [DD-036](DD-036.md) | Notifiche email via Gmail | +| [DD-037](DD-037.md) | Promemoria evento senza ritorni | +| [DD-038](DD-038.md) | Tre canali per ogni notifica | +| [DD-039](DD-039.md) | Niente promemoria all'allenatore | +| [DD-040](DD-040.md) | Palloni e solleciti automatici | +| [DD-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni | **In valutazione** diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index 785fc6f..0e00435 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -82,7 +82,7 @@ sulla tab Stagione. ### Avviso certificati -**Stato:** implementato (DD-035). +**Stato:** implementato (DD-035, soglia del giocatore DD-041). Un avviso in Home segnala i certificati medici in scadenza o scaduti, così nessuno se ne accorge quando il giocatore è già fuori regola. Si calcola al volo dalla data di scadenza @@ -90,13 +90,13 @@ già salvata: niente tabella, niente migration, niente push. #### Chi lo vede -| Utente | Cosa vede | Al tocco | -| --------------------------- | --------------------------------------------------------------- | ----------------------------- | -| Admin | l'avviso **dello staff**: i nomi di tutti i giocatori coinvolti | niente, non è cliccabile | -| Giocatore interessato | l'avviso **personale**: solo il proprio certificato | apre `/profilo?tab=documenti` | -| Admin che è anche giocatore | solo l'avviso dello staff, dove compare già il suo nome | niente | -| Altri giocatori | niente | — | -| Allenatore | niente: non vede i certificati altrui e non ne ha uno (DD-034) | — | +| Utente | Cosa vede | Al tocco | +| --------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------- | +| Admin | l'avviso **dello staff**: i nomi di tutti i giocatori coinvolti | niente, non è cliccabile | +| Giocatore interessato | l'avviso **personale**: solo il proprio certificato | apre `/profilo?tab=documenti` | +| Admin che è anche giocatore | **entrambi**: lo staff (senza il suo nome) e il proprio personale, a 30 giorni | personale: apre `/profilo?tab=documenti` | +| Altri giocatori | niente | — | +| Allenatore | niente: non vede i certificati altrui e non ne ha uno (DD-034) | — | L'avviso dello staff non è cliccabile, come quello dei palloni: l'admin non può caricare il certificato al posto del giocatore (DD-017), quindi può solo sollecitarlo, e il giocatore @@ -109,15 +109,24 @@ solo proprio a un giocatore. #### Quando compare -`giorni` è la differenza in giorni di calendario tra la scadenza e oggi (ora locale), soglia -`GIORNI_AVVISO_CERTIFICATO = 7`. +`giorni` è la differenza in giorni di calendario tra la scadenza e oggi (ora locale). La +soglia del giallo dipende da chi guarda (DD-041): il giocatore è avvisato un mese prima dello +staff. -| Condizione | Stato | Avviso | -| -------------------- | ----------- | ------------------ | -| `giorni > 7` | valido | nessuno | -| `0 ≤ giorni ≤ 7` | in scadenza | giallo (`warning`) | -| `giorni < 0` | scaduto | nero (`primary`) | -| data o file mancanti | mancante | nessuno | +| Chi guarda | Soglia | +| ---------- | ------------------------------------------ | +| Giocatore | `GIORNI_AVVISO_CERTIFICATO_GIOCATORE = 30` | +| Admin | `GIORNI_AVVISO_CERTIFICATO = 7` | + +| Condizione | Stato | Avviso | +| --------------------- | ----------- | ------------------ | +| `giorni > soglia` | valido | nessuno | +| `0 ≤ giorni ≤ soglia` | in scadenza | giallo (`warning`) | +| `giorni < 0` | scaduto | nero (`primary`) | +| data o file mancanti | mancante | nessuno | + +Esempio: scadenza il 5 ottobre 2026. Dal 5 settembre l'avviso giallo compare solo al +giocatore; dal 28 settembre anche agli admin; dal 6 ottobre è nero per entrambi. - Il giorno della scadenza il certificato vale ancora, coerente con `statoScadenza()`: il giocatore compare nel giallo con «scade oggi» e passa al nero dal giorno dopo. @@ -136,7 +145,7 @@ solo proprio a un giocatore. | Giallo, personale | «Certificato in scadenza» | «Il tuo certificato medico scade tra 5 giorni» / «domani» / «oggi» | | Nero, personale | «Certificato scaduto» | «Il tuo certificato medico è scaduto il 12/09/2026: caricane uno nuovo» | -Il numero di giorni scende da solo ogni giorno (7, 6, 5… domani, oggi): l'avviso non +Il numero di giorni scende da solo ogni giorno (30… 7, 6, 5… domani, oggi): l'avviso non conserva niente, rilegge la data a ogni apertura della Home. #### Più giocatori @@ -182,14 +191,16 @@ la Home non deve rompersi per un dato accessorio. #### Implementazione -| Pezzo | Ruolo | -| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `GIORNI_AVVISO_CERTIFICATO` (`profili-core.ts`) | la soglia, 7 | -| `giorniAllaScadenza(scadenza, oggi)` | differenza in giorni di calendario tra due date `AAAA-MM-GG`, negativa se scaduto | -| `avvisiCertificati(rosa, profili, oggi)` | funzione pura: restituisce `{ scaduti, inScadenza }`, ognuno una lista ordinata di `{ giocatoreId, nome, cognome, scadenza, giorni }` | -| `testoScadenza(giorni)` | «scade oggi» / «scade domani» / «scade tra N giorni» | -| `src/components/crapp/AvvisoCertificati.tsx` | legge `useGiocatoriSquadra()`, `useProfili()`, `useIsAdmin()` e l'utente corrente; sceglie avviso dello staff o personale e disegna le card | -| `src/routes/index.tsx` | monta `` subito dopo `` | +| Pezzo | Ruolo | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GIORNI_AVVISO_CERTIFICATO` (`profili-core.ts`) | la soglia dello staff, 7 | +| `GIORNI_AVVISO_CERTIFICATO_GIOCATORE` | la soglia del giocatore, 30 (DD-041) | +| `giorniAllaScadenza(scadenza, oggi)` | differenza in giorni di calendario tra due date `AAAA-MM-GG`, negativa se scaduto | +| `avvisiCertificati(rosa, profili, oggi, soglia)` | funzione pura (`soglia` di default 7): restituisce `{ scaduti, inScadenza }`, ognuno una lista ordinata di `{ giocatoreId, nome, cognome, scadenza, giorni }` | +| `avvisiCertificatiUtente(rosa, profili, oggi, utente)` | funzione pura: `{ personale, staff }` per chi guarda; l'admin giocatore ha entrambi, senza il suo nome nello staff (DD-041) | +| `testoScadenza(giorni)` | «scade oggi» / «scade domani» / «scade tra N giorni» | +| `src/components/crapp/AvvisoCertificati.tsx` | legge `useGiocatoriSquadra()`, `useProfili()`, `useIsAdmin()` e l'utente corrente; sceglie avviso dello staff o personale e disegna le card | +| `src/routes/index.tsx` | monta `` subito dopo `` | - "Oggi" viene da `oggiISO()` (`palloni-core.ts`), in ora locale. Non da `new Date().toISOString()`, che è in UTC e tra mezzanotte e le 2 darebbe il giorno prima. @@ -197,7 +208,7 @@ la Home non deve rompersi per un dato accessorio. `/admin`, quindi al massimo una lettura di `profili_giocatore` per sessione (vedi [EFFICIENZA_CLOUD.md](../EFFICIENZA_CLOUD.md)). Per l'allenatore il componente non legge i profili. -- Test in `test/unit/profili-core.test.ts`: soglia (8 giorni no, 7 sì, 0 sì, −1 scaduto), +- Test in `test/unit/profili-core.test.ts`: avvisi per utente (admin giocatore, admin puro, giocatore semplice), soglia dello staff (8 giorni no, 7 sì, 0 sì, −1 scaduto) e del giocatore (31 no, 30 sì), certificato senza file o senza data escluso, giocatore disattivato e allenatore esclusi, ordinamento (per data, poi alfabetico), testi al singolare e al plurale, cambio di mese e di anno nel calcolo dei giorni. @@ -254,8 +265,8 @@ Upload. Il giocatore può aggiornare liberamente sia la data sia il file. -Quando mancano 7 giorni o meno alla scadenza, o il certificato è scaduto, il giocatore -riceve un avviso nella propria Home e compare in quello degli admin (vedi +Quando mancano 30 giorni o meno alla scadenza, o il certificato è scaduto, il giocatore +riceve un avviso nella propria Home; compare in quello degli admin da 7 giorni prima (vedi [Avviso certificati](#avviso-certificati)). Lo storico non viene mantenuto nella prima versione. diff --git a/src/components/crapp/AvvisoCertificati.tsx b/src/components/crapp/AvvisoCertificati.tsx index 94b4ac9..d0a68ad 100644 --- a/src/components/crapp/AvvisoCertificati.tsx +++ b/src/components/crapp/AvvisoCertificati.tsx @@ -2,7 +2,7 @@ import { AlertCircle } from "lucide-react"; import { Link } from "@tanstack/react-router"; import { useProfili } from "@/lib/profili"; import { - avvisiCertificati, + avvisiCertificatiUtente, formatDataBreve, testoScadenza, type AvvisoCertificato, @@ -49,7 +49,10 @@ function Card({ ); } -/** Avviso certificati in scadenza/scaduti (DD-035): calcolato al volo, niente push. */ +/** + * Avviso certificati in scadenza/scaduti (DD-035, DD-041): calcolato al volo, niente push. + * Il giocatore lo vede da 30 giorni prima della scadenza, lo staff da 7. + */ export function AvvisoCertificati() { const admin = useIsAdmin(); const ruoliPronti = useRuoliPronti(); @@ -61,50 +64,48 @@ export function AvvisoCertificati() { const oggi = oggiISO(); - if (admin) { - const { scaduti, inScadenza } = avvisiCertificati(rosa, profili, oggi); - if (scaduti.length === 0 && inScadenza.length === 0) return null; - return ( - <> - {scaduti.length > 0 ? ( - rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} - /> - ) : null} - {inScadenza.length > 0 ? ( - rigaStaff(a, testoScadenza(a.giorni)))} - /> - ) : null} - - ); - } + // Admin che è anche giocatore: vede il proprio avviso a 30 giorni e quello dello staff (DD-041). + const { personale, staff } = avvisiCertificatiUtente(rosa, profili, oggi, { admin, base }); + const cardPersonale = personale ? ( + personale.giorni < 0 ? ( + + ) : ( + + ) + ) : null; - if (!base) return null; - const { scaduti, inScadenza } = avvisiCertificati([base], profili, oggi); - const mio = scaduti[0] ?? inScadenza[0]; - if (!mio) return null; - - return mio.giorni < 0 ? ( - - ) : ( - + if (!staff) return cardPersonale; + const { scaduti, inScadenza } = staff; + return ( + <> + {cardPersonale} + {scaduti.length > 0 ? ( + rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} + /> + ) : null} + {inScadenza.length > 0 ? ( + rigaStaff(a, testoScadenza(a.giorni)))} + /> + ) : null} + ); } diff --git a/src/lib/profili-core.ts b/src/lib/profili-core.ts index aff48d6..0841b58 100644 --- a/src/lib/profili-core.ts +++ b/src/lib/profili-core.ts @@ -216,9 +216,12 @@ export function etichettaGiocatore(g: GiocatoreSquadra): string { return `#${g.numero} ${nomeCompleto(g)}`; } -/** Soglia dell'avviso certificati in Home (DD-035). */ +/** Soglia dell'avviso certificati dello staff in Home (DD-035). */ export const GIORNI_AVVISO_CERTIFICATO = 7; +/** Soglia dell'avviso personale: il giocatore è avvisato un mese prima dello staff (DD-041). */ +export const GIORNI_AVVISO_CERTIFICATO_GIOCATORE = 30; + /** Giorni di calendario tra due date `AAAA-MM-GG` (ora locale): negativo se `scadenza` è passata. */ export function giorniAllaScadenza(scadenza: string, oggi: string): number { const a = new Date(`${scadenza}T00:00:00`); @@ -258,11 +261,13 @@ function confrontaAvvisi(a: AvvisoCertificato, b: AvvisoCertificato): number { * Certificati in scadenza o scaduti nella rosa (DD-035): calcolato al volo da * `certificato_scadenza`, niente stato salvato. Esclude chi non è in rosa e chi non ha * ancora un certificato caricato (è un problema diverso, non un avviso di scadenza). + * `soglia` sono i giorni di preavviso: 7 per lo staff, 30 per il giocatore (DD-041). */ export function avvisiCertificati( rosa: GiocatoreSquadra[], profili: Record, oggi: string, + soglia: number = GIORNI_AVVISO_CERTIFICATO, ): { scaduti: AvvisoCertificato[]; inScadenza: AvvisoCertificato[] } { const scaduti: AvvisoCertificato[] = []; const inScadenza: AvvisoCertificato[] = []; @@ -271,7 +276,7 @@ export function avvisiCertificati( const p = profili[g.id]; if (!p?.certificatoScadenza || !p.certificatoPath) continue; const giorni = giorniAllaScadenza(p.certificatoScadenza, oggi); - if (giorni > GIORNI_AVVISO_CERTIFICATO) continue; + if (giorni > soglia) continue; const avviso: AvvisoCertificato = { giocatoreId: g.id, nome: g.nome, @@ -285,3 +290,32 @@ export function avvisiCertificati( inScadenza.sort(confrontaAvvisi); return { scaduti, inScadenza }; } + +/** + * Avvisi certificati per chi guarda la Home (DD-035, DD-041): `personale` è il proprio + * certificato (soglia 30 giorni), per chiunque sia un giocatore in rosa; `staff` sono gli + * altri della rosa (soglia 7 giorni), solo per l'admin, senza il suo nome perché già nel + * personale. + */ +export function avvisiCertificatiUtente( + rosa: GiocatoreSquadra[], + profili: Record, + oggi: string, + utente: { admin: boolean; base: GiocatoreSquadra | null }, +): { + personale: AvvisoCertificato | undefined; + staff: { scaduti: AvvisoCertificato[]; inScadenza: AvvisoCertificato[] } | null; +} { + const mio = utente.base + ? avvisiCertificati([utente.base], profili, oggi, GIORNI_AVVISO_CERTIFICATO_GIOCATORE) + : null; + const personale = mio ? (mio.scaduti[0] ?? mio.inScadenza[0]) : undefined; + const staff = utente.admin + ? avvisiCertificati( + rosa.filter((g) => g.id !== utente.base?.id), + profili, + oggi, + ) + : null; + return { personale, staff }; +} diff --git a/test/unit/profili-core.test.ts b/test/unit/profili-core.test.ts index aa9f2a8..7657399 100644 --- a/test/unit/profili-core.test.ts +++ b/test/unit/profili-core.test.ts @@ -3,6 +3,8 @@ import assert from "node:assert/strict"; import { aRigaProfilo, avvisiCertificati, + avvisiCertificatiUtente, + GIORNI_AVVISO_CERTIFICATO_GIOCATORE, completamento, completamentoAllenatore, csvTesseramento, @@ -333,4 +335,78 @@ assert.deepEqual( "il più vecchio prima, poi alfabetico a parità di data (Alfa prima di Zeta)", ); +// soglia del giocatore (DD-041): 30 giorni di preavviso invece di 7 +const rosaMese: GiocatoreSquadra[] = [ + { ...squadra[0]!, id: "g1", nome: "Ivan", cognome: "Cacciari" }, + { ...squadra[0]!, id: "g2", nome: "Anna", cognome: "Bruni" }, + { ...squadra[0]!, id: "g3", nome: "Marco", cognome: "Verdi" }, +]; +const profiliMese: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-10-21", certificatoPath: "p1" }, // 31 giorni: fuori + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-10-20", certificatoPath: "p2" }, // 30 giorni: dentro + g3: { ...vuoto, giocatoreId: "g3", certificatoScadenza: "2026-09-10", certificatoPath: "p3" }, // scaduto +}; +assert.equal(GIORNI_AVVISO_CERTIFICATO_GIOCATORE, 30); +const mese = avvisiCertificati( + rosaMese, + profiliMese, + oggiTest, + GIORNI_AVVISO_CERTIFICATO_GIOCATORE, +); +assert.deepEqual( + mese.inScadenza.map((a) => a.giocatoreId), + ["g2"], + "31 giorni resta valido, 30 genera l'avviso del giocatore", +); +assert.deepEqual( + mese.scaduti.map((a) => a.giocatoreId), + ["g3"], +); +const staffMese = avvisiCertificati(rosaMese, profiliMese, oggiTest); +assert.equal(staffMese.inScadenza.length, 0, "senza soglia esplicita resta 7 giorni (staff)"); +assert.deepEqual( + staffMese.scaduti.map((a) => a.giocatoreId), + ["g3"], +); + +// avvisi per utente (DD-041): admin giocatore, admin puro, giocatore semplice +const rosaUtente: GiocatoreSquadra[] = [ + { ...squadra[0]!, id: "g1", nome: "Ivan", cognome: "Cacciari" }, + { ...squadra[0]!, id: "g2", nome: "Anna", cognome: "Bruni" }, +]; +const profiliUtente: Record = { + g1: { ...vuoto, giocatoreId: "g1", certificatoScadenza: "2026-10-10", certificatoPath: "p1" }, // 20 giorni + g2: { ...vuoto, giocatoreId: "g2", certificatoScadenza: "2026-09-25", certificatoPath: "p2" }, // 5 giorni +}; +const adminGiocatore = avvisiCertificatiUtente(rosaUtente, profiliUtente, oggiTest, { + admin: true, + base: rosaUtente[0]!, +}); +assert.equal( + adminGiocatore.personale?.giocatoreId, + "g1", + "admin giocatore: avviso personale a 30 giorni", +); +assert.deepEqual( + adminGiocatore.staff?.inScadenza.map((a) => a.giocatoreId), + ["g2"], + "lo staff non ripete il suo nome", +); +const adminPuro = avvisiCertificatiUtente(rosaUtente, profiliUtente, oggiTest, { + admin: true, + base: null, +}); +assert.equal(adminPuro.personale, undefined); +assert.deepEqual( + adminPuro.staff?.inScadenza.map((a) => a.giocatoreId), + ["g2"], + "admin non giocatore: solo staff a 7 giorni (g1 a 20 giorni escluso)", +); +const soloGiocatore = avvisiCertificatiUtente(rosaUtente, profiliUtente, oggiTest, { + admin: false, + base: rosaUtente[0]!, +}); +assert.equal(soloGiocatore.personale?.giocatoreId, "g1"); +assert.equal(soloGiocatore.staff, null, "il giocatore semplice non vede lo staff"); + console.log("profili-core: ok");