Avviso certificati: il giocatore è avvisato 30 giorni prima (DD-041)
Il giocatore vede l'avviso giallo da 30 giorni prima della scadenza del certificato medico; per lo staff resta 7 giorni, nero dopo la scadenza per tutti. L'admin che è anche giocatore vede il proprio avviso personale oltre a quello dello staff, da cui è escluso il suo nome. La scelta degli avvisi per utente è in avvisiCertificatiUtente() (profili-core) con test; documentati DD-041, DD-035, modulo e changelog. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f25143aa5
commit
4b5ffc6664
8 files changed
+285
-117
No files matched your search
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
+41
-40
@@ -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**
|
||||
|
||||
|
||||
@@ -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 `<AvvisoCertificati />` subito dopo `<PromemoriaPalloni />` |
|
||||
| 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 `<AvvisoCertificati />` subito dopo `<PromemoriaPalloni />` |
|
||||
|
||||
- "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.
|
||||
|
||||
@@ -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 ? (
|
||||
<Card
|
||||
colore="primary"
|
||||
titolo="Certificati scaduti"
|
||||
righe={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))}
|
||||
/>
|
||||
) : null}
|
||||
{inScadenza.length > 0 ? (
|
||||
<Card
|
||||
colore="warning"
|
||||
titolo="Certificati in scadenza"
|
||||
righe={inScadenza.map((a) => 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 ? (
|
||||
<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 (!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)}`]}
|
||||
/>
|
||||
if (!staff) return cardPersonale;
|
||||
const { scaduti, inScadenza } = staff;
|
||||
return (
|
||||
<>
|
||||
{cardPersonale}
|
||||
{scaduti.length > 0 ? (
|
||||
<Card
|
||||
colore="primary"
|
||||
titolo="Certificati scaduti"
|
||||
righe={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))}
|
||||
/>
|
||||
) : null}
|
||||
{inScadenza.length > 0 ? (
|
||||
<Card
|
||||
colore="warning"
|
||||
titolo="Certificati in scadenza"
|
||||
righe={inScadenza.map((a) => rigaStaff(a, testoScadenza(a.giorni)))}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+36
-2
@@ -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<string, Profilo>,
|
||||
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<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 };
|
||||
}
|
||||
@@ -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<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");
|
||||
Reference in new issue
Block a user