From c7445cfab18418535b6df8547d3bfdc31c0ad046 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Wed, 2 Sep 2026 15:27:12 +0200 Subject: [PATCH] Aggiunge il tracciamento del tesseramento CSI in giocatori_squadra Numero e data della tessera arrivano dal comitato dopo l'iscrizione, quindi li scrive solo un admin (come numero/ruolo): estende il trigger di M1/M5, aggiunge il pannello dedicato in /admin con badge e conteggio in dashboard. Co-Authored-By: Claude Sonnet 5 --- docs/CHANGELOG.md | 4 ++ docs/DATABASE.md | 2 +- docs/ROADMAP.md | 4 +- docs/modules/profilo-giocatore.md | 11 ++++ src/lib/giocatori-squadra.ts | 46 ++++++++++++- src/routes/admin.tsx | 66 ++++++++++++++++++- .../20260902120000_m8_tesseramento_csi.sql | 52 +++++++++++++++ test/integration/schema-profili.test.ts | 16 +++++ test/unit/giocatori-squadra.test.ts | 2 + test/unit/profili-core.test.ts | 4 ++ 10 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 supabase/migrations/20260902120000_m8_tesseramento_csi.sql diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 871ec07..2d90b68 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -28,6 +28,10 @@ qui: sta in [ROADMAP.md](ROADMAP.md). riga non viene mai eliminata, così presenze, voti, pagelle e badge della stagione restano agganciati al suo id. L'email è anche modificabile dal pannello "Dati squadra" di ogni giocatore già in rosa. +- Tracciamento tesseramento CSI (migration `m8_tesseramento_csi`): numero e data di tessera + in `giocatori_squadra`, come gli altri campi che gestisce solo l'admin (DD-016/DD-018). La + dashboard mostra chi è già tesserato (badge sulla scheda, contatore in "Squadra") e un + pannello per registrare numero e data una volta arrivata la tessera dal CSI. **Da applicare solo a squadra collegata**, altrimenti chi non ha ancora fatto login vede l'app vuota. - Profilo giocatore: da `/profilo` ognuno compila i propri dati anagrafici e carica diff --git a/docs/DATABASE.md b/docs/DATABASE.md index bef12fc..2a8ab16 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -9,7 +9,7 @@ non in questo file. | Tabella | Scopo | Note | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `giocatori_squadra` | Anagrafica operativa della squadra, con ID testuali (`g1`…`gN`), dati gestiti dagli admin (nome, cognome, numero, ruolo), collegamento all'account (`auth_user_id`) ed email registrata (`email`). | Introdotta dalla migration `m1_giocatori_squadra`, già popolata (17 giocatori) ma **non ancora letta dal codice**: la rosa arriva tuttora da `src/lib/crapp-data.ts`, che resta il fallback anche dopo il passaggio. Destinata a diventare la source of truth. Vedi DD-015 e DD-016. La colonna `email` (migration `m5_email_giocatori_squadra`) è la chiave del collegamento automatico account↔giocatore al primo accesso (DD-018): NULL finché non nota, oggi impostata solo per 2 dei 17 giocatori. | +| `giocatori_squadra` | Anagrafica operativa della squadra, con ID testuali (`g1`…`gN`), dati gestiti dagli admin (nome, cognome, numero, ruolo), collegamento all'account (`auth_user_id`) ed email registrata (`email`). | Introdotta dalla migration `m1_giocatori_squadra`, già popolata (17 giocatori) ma **non ancora letta dal codice**: la rosa arriva tuttora da `src/lib/crapp-data.ts`, che resta il fallback anche dopo il passaggio. Destinata a diventare la source of truth. Vedi DD-015 e DD-016. La colonna `email` (migration `m5_email_giocatori_squadra`) è la chiave del collegamento automatico account↔giocatore al primo accesso (DD-018): NULL finché non nota, oggi impostata solo per 2 dei 17 giocatori. Le colonne `numero_tessera`/`data_tessera` (migration `m8_tesseramento_csi`) tracciano chi è già tesserato al CSI; come `numero`/`ruolo` le scrive solo un admin, il trigger di M1/M5 le include tra i campi bloccati per chi reclama il proprio slot. | | `giocatori` | Anagrafica giocatori con UUID. | Presente ma **non usata** dal codice attuale: la convergenza è rinviata (DD-012, DD-014). | | `profili_giocatore` | Dati personali, metadati del documento d'identità, certificato medico e path dei file, in relazione 1:1 con `giocatori_squadra`. | Creata dalla migration `m2_profili_giocatore` (DD-016). Letta da `src/lib/profili.ts`; le policy mostrano al giocatore solo il proprio profilo e all'admin tutti. I file non stanno qui: la tabella conserva i path nel bucket. | | `user_roles` | Ruoli applicativi (es. amministratore, giocatore). | Fonte dei permessi di amministrazione, letta da `src/lib/ruoli.ts` (DD-011). Il primo admin va inserito a mano; vedi [PROJECT_STATE.md](../PROJECT_STATE.md). | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3e38358..216bd4d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -24,8 +24,8 @@ di attivazione sta in [PROJECT_STATE.md](../PROJECT_STATE.md). - [x] Certificati medici — caricamento, scadenza, stato e download; lo storico dei certificati resta un'estensione futura -- [ ] Gestione tesseramenti CSI — raccolta dati ed export CSV pronti, manca il tracciamento - di chi è già tesserato (numero e data di tessera) +- [x] Gestione tesseramenti CSI — raccolta dati, export CSV e tracciamento di chi è già + tesserato (numero e data di tessera) - [x] Dashboard amministratore - [x] Download CSV dati diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md index d43544b..0f3df10 100644 --- a/docs/modules/profilo-giocatore.md +++ b/docs/modules/profilo-giocatore.md @@ -158,6 +158,7 @@ Per ogni giocatore vengono mostrati. - Certificato medico - Documento di identità - Foto tessera +- Stato tesseramento CSI (tesserato / da tesserare) Azioni disponibili. @@ -166,6 +167,7 @@ Azioni disponibili. - Scarica documento - Scarica foto tessera - Modifica dati squadra e dati personali del giocatore (DD-017) +- Registra numero e data della tessera CSI, una volta arrivata dal comitato - Scollega account, per liberare uno slot assegnato per errore - Aggiungi giocatore, per inserire un nuovo membro della squadra - Disattiva/Riattiva giocatore, per chi lascia la squadra (o rientra) @@ -189,6 +191,15 @@ Campi esportati. - Data emissione - Data scadenza +## Tracciamento tesseramento + +Numero e data della tessera CSI non sono dati che il giocatore conosce in anticipo: arrivano +dal comitato dopo l'iscrizione effettiva. Per questo, a differenza dei dati personali del +profilo, li scrive solo un amministratore — come nome, cognome, numero di maglia e ruolo +(DD-017), il trigger sulla tabella li rende non modificabili dal giocatore stesso. La +dashboard mostra un badge "Tesserato"/"Da tesserare" su ogni scheda e il conteggio +complessivo della squadra. + ## Completamento profilo Ogni sezione contribuisce alla percentuale di completamento. diff --git a/src/lib/giocatori-squadra.ts b/src/lib/giocatori-squadra.ts index 3a598ee..1de16d6 100644 --- a/src/lib/giocatori-squadra.ts +++ b/src/lib/giocatori-squadra.ts @@ -16,6 +16,8 @@ export type GiocatoreSquadra = { authUserId: string | null; attivo: boolean; email: string | null; + numeroTessera: string | null; + dataTessera: string | null; }; type RigaGiocatoreSquadra = { @@ -27,6 +29,8 @@ type RigaGiocatoreSquadra = { auth_user_id: string | null; attivo: boolean; email: string | null; + numero_tessera: string | null; + data_tessera: string | null; }; export const SQUADRA_KEY = ["giocatori-squadra"] as const; @@ -41,6 +45,8 @@ export function rosaFallback(): GiocatoreSquadra[] { authUserId: null, attivo: true, email: null, + numeroTessera: null, + dataTessera: null, })); } @@ -70,7 +76,9 @@ export function slotPerEmail( async function fetchSquadra(): Promise { const { data, error } = await supabaseNuoveTabelle .from("giocatori_squadra") - .select("id, nome, cognome, numero, ruolo, auth_user_id, attivo, email") + .select( + "id, nome, cognome, numero, ruolo, auth_user_id, attivo, email, numero_tessera, data_tessera", + ) .order("cognome") .order("nome"); if (error) throw error; @@ -84,6 +92,8 @@ async function fetchSquadra(): Promise { authUserId: r.auth_user_id, attivo: r.attivo, email: r.email, + numeroTessera: r.numero_tessera, + dataTessera: r.data_tessera, })); } @@ -158,6 +168,40 @@ export function useSalvaDatiSquadra() { }); } +/** Numero e data della tessera CSI, note solo dopo il tesseramento effettivo. */ +export type DatiTesseramento = Pick; + +/** + * Registra numero e data della tessera CSI (roadmap v1.1). Campo puramente amministrativo: + * il trigger di M8 lo rende scrivibile solo da un admin, il giocatore non può autodichiararsi + * tesserato. + */ +export function useSalvaTesseramento() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (input: { giocatoreId: string; dati: DatiTesseramento }) => { + const dati = { + numero_tessera: input.dati.numeroTessera?.trim() || null, + data_tessera: input.dati.dataTessera || null, + }; + const { error } = await supabaseNuoveTabelle + .from("giocatori_squadra") + .update(dati) + .eq("id", input.giocatoreId); + if (error) throw error; + return { + giocatoreId: input.giocatoreId, + dati: { numeroTessera: dati.numero_tessera, dataTessera: dati.data_tessera }, + }; + }, + onSuccess: (input) => { + queryClient.setQueryData(SQUADRA_KEY, (prec) => + (prec ?? []).map((g) => (g.id === input.giocatoreId ? { ...g, ...input.dati } : g)), + ); + }, + }); +} + /** * Aggiunge un giocatore alla rosa (DD-017). Solo un admin passa le policy di M1. * L'id (`g`) non è generato dal database: va calcolato con `prossimoIdGiocatore` diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index 147e88f..2d2c400 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { + BadgeCheck, ChevronDown, Download, FileText, @@ -25,9 +26,11 @@ import { useGiocatoriSquadra, useImpostaAttivo, useSalvaDatiSquadra, + useSalvaTesseramento, useScollegaAccount, validaDatiSquadra, type DatiSquadra, + type DatiTesseramento, type GiocatoreSquadra, } from "@/lib/giocatori-squadra"; import { scaricaFile, useProfili, useSalvaProfilo } from "@/lib/profili"; @@ -102,11 +105,18 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi const { righe } = useGiocatoriSquadra(); const salvaSquadra = useSalvaDatiSquadra(); const salvaProfilo = useSalvaProfilo(); + const salvaTesseramento = useSalvaTesseramento(); const scollega = useScollegaAccount(); const impostaAttivo = useImpostaAttivo(); const [datiSquadra, setDatiSquadra] = useState(null); const [bozza, setBozza] = useState(null); + const [tesseramento, setTesseramento] = useState(null); + + const tesseramentoCorrente: DatiTesseramento = tesseramento ?? { + numeroTessera: g.numeroTessera, + dataTessera: g.dataTessera, + }; const squadraCorrente: DatiSquadra = datiSquadra ?? { nome: g.nome, @@ -148,6 +158,16 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi } } + async function confermaTesseramento() { + try { + await salvaTesseramento.mutateAsync({ giocatoreId: g.id, dati: tesseramentoCorrente }); + setTesseramento(null); + toast.success("Tesseramento aggiornato"); + } catch (e) { + toast.error(messaggioErrore(e, "Salvataggio non riuscito")); + } + } + async function confermaProfilo() { try { await salvaProfilo.mutateAsync(profiloCorrente); @@ -229,6 +249,39 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi Salva dati squadra +

Tesseramento CSI

+
+ + + setTesseramento({ ...tesseramentoCorrente, numeroTessera: e.target.value || null }) + } + className={classiInput} + /> + + + + setTesseramento({ ...tesseramentoCorrente, dataTessera: e.target.value || null }) + } + className={classiInput} + /> + +
+ + setBozza({ ...profiloCorrente, ...patch })} @@ -401,6 +454,15 @@ function SchedaGiocatore({ stato={sezioni.foto ? "presente" : "assente"} path={profilo?.fotoPath ?? null} /> + + + {g.numeroTessera ? "Tesserato" : "Da tesserare"} + {aperta ? : null} @@ -581,13 +643,14 @@ function Dashboard() { statoScadenza(profili[g.id]?.certificatoScadenza, profili[g.id]?.certificatoPath, oggi) === "valido", ).length; + const tesserati = attivi.filter((g) => g.numeroTessera).length; return ( <>
-
+
+