From 519f9aaff4dae7c65b68d5c7872caee9c7f89654 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Tue, 6 Oct 2026 11:44:58 +0200 Subject: [PATCH] =?UTF-8?q?Avvisi=20in=20Home:=20componente=20condiviso,?= =?UTF-8?q?=20pi=C3=B9=20leggibili;=20turno=20palloni=20ridisegnato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Certificati e turno palloni usano lo stesso componente Avviso: icona propria per ogni caso (il colore non è l'unico segnale), testo più grande, elenchi di persone in righe con dettaglio a destra e conteggio, freccia sulle card cliccabili, comparsa graduale. Il turno palloni ha la pillola «Oggi»/«In arrivo» e i messaggi in riquadri. Solo aspetto: testi e regole di comparsa invariati. Test del componente, CHANGELOG, ARCHITECTURE e test/README aggiornati. Co-Authored-By: Claude Sonnet 5.5 --- docs/ARCHITECTURE.md | 2 +- docs/CHANGELOG.md | 4 + src/components/crapp/Avviso.tsx | 127 +++++++++++++++++++++ src/components/crapp/AvvisoCertificati.tsx | 89 +++++---------- src/components/crapp/PromemoriaPalloni.tsx | 24 ++-- test/README.md | 2 +- test/unit/avviso.test.ts | 84 ++++++++++++++ 7 files changed, 258 insertions(+), 74 deletions(-) create mode 100644 src/components/crapp/Avviso.tsx create mode 100644 test/unit/avviso.test.ts diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fcffcc2..614a699 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -85,7 +85,7 @@ quando il database non risponde. ## UI Componenti condivisi in `src/components/crapp/` (`ui-bits.tsx` per `Card`, `PageHeader`, -`Section`, `StatTile`; i drawer di dettaglio `BadgeDrawer` e `ObiettivoDrawer`), animazioni in `src/components/motion/`. Mobile-first (DD-005): poche +`Section`, `StatTile`; `Avviso.tsx` per gli avvisi colorati in Home; i drawer di dettaglio `BadgeDrawer` e `ObiettivoDrawer`), animazioni in `src/components/motion/`. Mobile-first (DD-005): poche schermate, pochi click. In `src/components/ui/` restano solo le due primitive shadcn davvero usate, `drawer` (vaul) e diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1adedfa..2c855e6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -25,6 +25,10 @@ versioni `0.y.z` erano pre-release. ### Modificato +- **Avvisi in Home più leggibili** — certificati e turno palloni usano lo stesso componente (`Avviso`): icona + propria per ogni caso (non solo il colore), titolo e testo più grandi, elenchi di persone in righe con il dettaglio + a destra e il conteggio, freccia dove la card porta ai documenti, comparsa graduale. Solo aspetto: testi e regole + di quando compaiono non cambiano. Nessuna migration. - **Tessera CSI tolta dalla dashboard** — in Profili spariscono il blocco «Tesseramento CSI» (numero e data tessera), il badge «Tesserato»/«Da tesserare» e il riquadro «Tesserati» della tab Squadra. L'export CSV non cambia. Le colonne a database restano, non più usate (DD-048). Nessuna migration. diff --git a/src/components/crapp/Avviso.tsx b/src/components/crapp/Avviso.tsx new file mode 100644 index 0000000..48b28cb --- /dev/null +++ b/src/components/crapp/Avviso.tsx @@ -0,0 +1,127 @@ +import { Link } from "@tanstack/react-router"; +import { ChevronRight, type LucideIcon } from "lucide-react"; +import { Reveal } from "@/components/motion/Reveal"; +import { cn } from "@/lib/utils"; + +/** + * Gravità di un avviso in Home. Il colore non è l'unico segnale: ogni avviso ha anche un'icona + * sua e un titolo, così si distingue anche senza vedere i colori. + */ +export type TonoAvviso = "critico" | "attenzione" | "scaduto" | "azione"; + +const toni: Record = { + critico: "bg-destructive text-destructive-foreground", + attenzione: "bg-warning text-warning-foreground", + scaduto: "bg-primary text-primary-foreground", + azione: "bg-accent-grad text-accent-foreground", +}; + +/** Una riga di un elenco: a sinistra chi, a destra il dettaglio (es. la scadenza). */ +export type RigaAvviso = { chi: string; cosa: string }; + +type ProprietaCorpo = { + tono: TonoAvviso; + icona: LucideIcon; + titolo: string; + /** Frasi di spiegazione, una per paragrafo. */ + testi?: string[]; + /** Elenco di persone con il loro dettaglio; con più di una riga compare il conteggio. */ + elenco?: RigaAvviso[]; + /** Mostra la freccia: tutta la card porta a un'altra schermata. */ + cliccabile?: boolean; + /** Parola breve accanto al titolo (es. «Oggi»), per dire subito quando riguarda. */ + etichetta?: string; + /** I testi in riquadri propri, uno per messaggio: per chi deve fare una cosa precisa. */ + riquadri?: boolean; +}; + +/** Contenuto dell'avviso, senza margini né link: è la parte che si può verificare nei test. */ +export function CorpoAvviso({ + tono, + icona: Icona, + titolo, + testi = [], + elenco = [], + cliccabile = false, + etichetta, + riquadri = false, +}: ProprietaCorpo) { + return ( +
+
+ + + +

+ {titolo} +

+ {etichetta ? ( + + {etichetta} + + ) : null} + {elenco.length > 1 ? ( + + {elenco.length} + + ) : null} + {cliccabile ? : null} +
+ + {testi.map((t) => ( +

+ {t} +

+ ))} + + {elenco.length > 0 ? ( +
    + {elenco.map((r) => ( +
  • + {r.chi} + {r.cosa} +
  • + ))} +
+ ) : null} +
+ ); +} + +/** + * Avviso in Home: comparsa graduale, margini della colonna e, se serve, link ai documenti del + * profilo. Tutti gli avvisi (certificati, turno palloni) passano da qui per avere lo stesso aspetto. + */ +export function Avviso({ indice = 0, ...corpo }: ProprietaCorpo & { indice?: number }) { + const card = ; + return ( + + {corpo.cliccabile ? ( + + {card} + + ) : ( +
{card}
+ )} +
+ ); +} diff --git a/src/components/crapp/AvvisoCertificati.tsx b/src/components/crapp/AvvisoCertificati.tsx index e9c1be4..a5c6f83 100644 --- a/src/components/crapp/AvvisoCertificati.tsx +++ b/src/components/crapp/AvvisoCertificati.tsx @@ -1,5 +1,5 @@ -import { AlertCircle } from "lucide-react"; -import { Link } from "@tanstack/react-router"; +import { CalendarClock, FileClock, FileX } from "lucide-react"; +import { Avviso, type RigaAvviso } from "@/components/crapp/Avviso"; import { useProfili } from "@/lib/profili"; import { avvisiCertificatiUtente, @@ -12,45 +12,6 @@ import { oggiISO } from "@/lib/palloni-core"; import { useIsAdmin, useRuoliPronti } from "@/lib/ruoli"; import { useGiocatoreBase } from "@/lib/user-store"; -function Card({ - colore, - titolo, - righe, - cliccabile, -}: { - colore: "warning" | "primary" | "destructive"; - titolo: string; - righe: string[]; - cliccabile?: boolean; -}) { - const classi = `mx-5 mt-4 block rounded-3xl p-4 shadow-pop ${ - colore === "warning" - ? "bg-warning text-warning-foreground" - : colore === "destructive" - ? "bg-destructive text-destructive-foreground" - : "bg-primary text-primary-foreground" - } ${cliccabile ? "premi" : ""}`; - const contenuto = ( - <> -

- {titolo} -

- {righe.map((r) => ( -

- {r} -

- ))} - - ); - return cliccabile ? ( - - {contenuto} - - ) : ( -
{contenuto}
- ); -} - /** * Avviso certificati mancanti/scaduti/in scadenza (DD-035, DD-041, DD-046): calcolato al volo, niente push. * Il giocatore lo vede da 30 giorni prima della scadenza, lo staff da 7. @@ -72,28 +33,31 @@ export function AvvisoCertificati() { base, }); const cardPersonale = personaleMancante ? ( - ) : personale ? ( personale.giorni < 0 ? ( - ) : ( - ) ) : null; @@ -104,30 +68,33 @@ export function AvvisoCertificati() { <> {cardPersonale} {mancanti.length > 0 ? ( - `${m.nome} ${m.cognome} — non caricato`)} + elenco={mancanti.map((m) => ({ chi: `${m.nome} ${m.cognome}`, cosa: "non caricato" }))} /> ) : null} {scaduti.length > 0 ? ( - rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} + elenco={scaduti.map((a) => rigaStaff(a, `scaduto il ${formatDataBreve(a.scadenza)}`))} /> ) : null} {inScadenza.length > 0 ? ( - rigaStaff(a, testoScadenza(a.giorni)))} + elenco={inScadenza.map((a) => rigaStaff(a, testoScadenza(a.giorni)))} /> ) : null} ); } -function rigaStaff(a: AvvisoCertificato, dettaglio: string): string { - return `${a.nome} ${a.cognome} — ${dettaglio}`; +function rigaStaff(a: AvvisoCertificato, dettaglio: string): RigaAvviso { + return { chi: `${a.nome} ${a.cognome}`, cosa: dettaglio }; } diff --git a/src/components/crapp/PromemoriaPalloni.tsx b/src/components/crapp/PromemoriaPalloni.tsx index 1de634a..807b923 100644 --- a/src/components/crapp/PromemoriaPalloni.tsx +++ b/src/components/crapp/PromemoriaPalloni.tsx @@ -1,4 +1,5 @@ -import { AlertCircle } from "lucide-react"; +import { Volleyball } from "lucide-react"; +import { Avviso } from "@/components/crapp/Avviso"; import { formatData } from "@/lib/crapp-data"; import { eventiPalloni, eventoPrecedente, eventoSuccessivo, oggiISO } from "@/lib/palloni-core"; import { useTurniPalloni } from "@/lib/palloni"; @@ -17,6 +18,8 @@ export function PromemoriaPalloni() { const oggi = oggiISO(); const messaggi: string[] = []; + // «Oggi» solo se c'è davvero qualcosa da fare oggi; altrimenti è un semplice promemoria. + let oggiTocca = true; for (const evento of lista) { if (evento.data !== oggi) continue; @@ -36,6 +39,7 @@ export function PromemoriaPalloni() { } if (messaggi.length === 0) { + oggiTocca = false; const prossimo = lista.find((e) => e.data >= oggi && turni[e.id] === io.id); if (!prossimo) return null; messaggi.push( @@ -44,15 +48,13 @@ export function PromemoriaPalloni() { } return ( -
-

- Turno palloni -

- {messaggi.map((m) => ( -

- {m} -

- ))} -
+ ); } diff --git a/test/README.md b/test/README.md index 6c78b7e..38faca7 100644 --- a/test/README.md +++ b/test/README.md @@ -26,7 +26,7 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push | Cartella | Cosa verifica | Serve rete? | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi (calcolo e card di dettaglio), 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 (calcolo e card di dettaglio), avvisi in Home, 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-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), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì | | `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì | | `helpers/` | Avvio del server di test e mini-harness condiviso | — | diff --git a/test/unit/avviso.test.ts b/test/unit/avviso.test.ts new file mode 100644 index 0000000..363db3c --- /dev/null +++ b/test/unit/avviso.test.ts @@ -0,0 +1,84 @@ +/** Check del componente degli avvisi in Home: `bun test/unit/avviso.test.ts`. */ +import assert from "node:assert/strict"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { CalendarClock, FileX } from "lucide-react"; +import { CorpoAvviso, type TonoAvviso } from "@/components/crapp/Avviso"; + +const html = (p: Parameters[0]) => + renderToStaticMarkup(createElement(CorpoAvviso, p)); + +// --- ogni tono ha il suo colore, e l'icona è decorativa (il titolo dice già tutto) --- +const classi: Record = { + critico: "bg-destructive", + attenzione: "bg-warning", + scaduto: "bg-primary", + azione: "bg-accent-grad", +}; +for (const [tono, classe] of Object.entries(classi) as [TonoAvviso, string][]) { + const h = html({ tono, icona: FileX, titolo: "Titolo" }); + assert.ok(h.includes(classe), `${tono}: classe colore ${classe}`); + assert.ok(h.includes("Titolo"), `${tono}: titolo`); + assert.ok(h.includes('aria-hidden="true"'), `${tono}: icona nascosta ai lettori di schermo`); +} + +// --- testi: un paragrafo per frase --------------------------------------------------- +const conTesti = html({ + tono: "attenzione", + icona: CalendarClock, + titolo: "Certificato in scadenza", + testi: ["Prima frase.", "Seconda frase."], +}); +assert.equal((conTesti.match(/