Toglie la foto tessera dal profilo (DD-044)

Il Profilo non chiede più la foto tessera e la scheda admin non la mostra.
Il completamento diventa dati personali 34%, documento 33%, certificato 33%.
La migration m29 elimina la colonna profili_giocatore.foto_path; le 4 foto
già caricate sono state cancellate dal bucket profili-giocatore. Migration
applicata in produzione.

Test: pesi e sezioni di profili-core, rendering di CampiProfilo, colonna
rimossa sul database locale. Documentati DD-044, modulo profilo-giocatore,
DATABASE, ROADMAP e changelog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-01 15:03:04 +02:00
1 parent e822e4574b
commit de78431090
21 files changed
+164 -79

No files matched your search

+5
View File
@@ -15,6 +15,11 @@ versioni `0.y.z` erano pre-release.
### 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).
+4 -4
View File
@@ -66,10 +66,10 @@ Colonne di `giocatori_squadra` che meritano una nota:
## Storage
| 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. |
| `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`. |
| Bucket | Scopo | Note |
| ------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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`. |
## Eventi e presenze
+1 -1
View File
@@ -27,7 +27,7 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`).
- [x] Dashboard amministratore
- [x] Download CSV dati
- [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
- [x] Gestione tesseramenti CSI — raccolta dati, export CSV e tracciamento di chi è già
tesserato (numero e data di tessera)
+1 -1
View File
@@ -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.
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.
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.
+1 -1
View File
@@ -15,7 +15,7 @@ Dalla dashboard amministratore, un admin può:
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.
**Alternative scartate**
+38
View File
@@ -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.
+1
View File
@@ -86,6 +86,7 @@ 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-039](DD-039.md) | Niente promemoria all'allenatore |
| [DD-040](DD-040.md) | Palloni e solleciti automatici |
| [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 |
+3 -3
View File
@@ -55,8 +55,8 @@ Un link diretto a `/profilo?tab=stagione` o `?tab=badge` apre Documenti.
| Telefono | `profili_giocatore.telefono` | |
| 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
tessera, tesseramento CSI.
**Non** si mostrano: indirizzo di residenza, documento d'identità, certificato medico,
tesseramento CSI.
### Completamento profilo
@@ -164,7 +164,7 @@ giocatore).
| ------------------------------------------------------------------- | ---------- | ------------------ | ----------------------------------- |
| Modificare nome e cognome | no | sì, i propri | tutti |
| 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 |
| Creare/modificare/eliminare eventi, convocati | no | sì | sì |
| Sollecitare le presenze | no | sì | sì |
+6 -12
View File
@@ -44,7 +44,7 @@ Può:
riga non viene eliminata, così presenze, voti, pagelle e badge della stagione restano
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.
## Flusso utente
@@ -73,7 +73,6 @@ Viene mostrata una barra di avanzamento (esempio: _Profilo completato — 85%_),
- Dati personali
- Documento di identità
- Certificato medico
- Foto tessera
Quando tutte le sezioni sono complete il widget scompare automaticamente.
@@ -273,9 +272,8 @@ Lo storico non viene mantenuto nella prima versione.
### Foto tessera
Upload di una fotografia formato tessera.
Utilizzata dagli amministratori per il tesseramento CSI.
**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.
### Statistiche
@@ -328,7 +326,6 @@ Per ogni giocatore, nella tab Profili, vengono mostrati.
- Stato del profilo
- Certificato medico
- Documento di identità
- Foto tessera
- Stato tesseramento CSI (tesserato / da tesserare)
Azioni disponibili.
@@ -336,7 +333,6 @@ Azioni disponibili.
- Visualizza profilo (la scheda si apre in linea nell'elenco: nessuna schermata separata)
- Scarica certificato
- 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
@@ -377,10 +373,9 @@ Ogni sezione contribuisce alla percentuale di completamento.
| Sezione | Peso |
| --------------------- | ---- |
| Dati personali | 30% |
| Documento di identità | 30% |
| Certificato medico | 30% |
| Foto tessera | 10% |
| Dati personali | 34% |
| Documento di identità | 33% |
| Certificato medico | 33% |
Quando tutte le sezioni risultano complete il profilo raggiunge il 100%.
@@ -402,7 +397,6 @@ nessun altro ne vede (vedi [Avviso certificati](#avviso-certificati)).
- Gestione dati personali
- Documento di identità
- Certificato medico
- Foto tessera
- Dashboard amministratore
- Esportazione CSV CSI
+2 -17
View File
@@ -120,14 +120,12 @@ export function CampiProfilo({
sezioni,
fileDocumento,
fileCertificato,
fileFoto,
}: {
corrente: Profilo;
aggiorna: (patch: Partial<Profilo>) => void;
sezioni: Record<Sezione, boolean>;
fileDocumento?: React.ReactNode;
fileCertificato?: React.ReactNode;
fileFoto?: React.ReactNode;
}) {
return (
<>
@@ -241,14 +239,13 @@ export function CampiProfilo({
/>
</Campo>
{fileCertificato}
{fileFoto}
</>
);
}
/**
* 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({
corrente,
@@ -469,18 +466,6 @@ export function ProfiloAmministrativo({
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
@@ -550,7 +535,7 @@ export function CompletaProfilo({
<p className="mt-2 text-[13px] text-muted-foreground">
{allenatore
? "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>
</Link>
</Reveal>
-3
View File
@@ -375,7 +375,6 @@ export type Database = {
documento_scadenza: string | null;
documento_tipo: string | null;
email: string | null;
foto_path: string | null;
giocatore_id: string;
indirizzo: string | null;
luogo_nascita: string | null;
@@ -395,7 +394,6 @@ export type Database = {
documento_scadenza?: string | null;
documento_tipo?: string | null;
email?: string | null;
foto_path?: string | null;
giocatore_id: string;
indirizzo?: string | null;
luogo_nascita?: string | null;
@@ -415,7 +413,6 @@ export type Database = {
documento_scadenza?: string | null;
documento_tipo?: string | null;
email?: string | null;
foto_path?: string | null;
giocatore_id?: string;
indirizzo?: string | null;
luogo_nascita?: string | null;
+4 -10
View File
@@ -21,7 +21,6 @@ export type Profilo = {
documentoRetroPath: string | null;
certificatoScadenza: string | null;
certificatoPath: string | null;
fotoPath: string | null;
};
export type RigaProfilo = {
@@ -40,11 +39,10 @@ export type RigaProfilo = {
documento_retro_path: string | null;
certificato_scadenza: string | null;
certificato_path: string | null;
foto_path: string | null;
};
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 {
return {
@@ -63,7 +61,6 @@ export function profiloVuoto(giocatoreId: string): Profilo {
documentoRetroPath: null,
certificatoScadenza: null,
certificatoPath: null,
fotoPath: null,
};
}
@@ -84,7 +81,6 @@ export function daRigaProfilo(r: RigaProfilo): Profilo {
documentoRetroPath: r.documento_retro_path,
certificatoScadenza: r.certificato_scadenza,
certificatoPath: r.certificato_path,
fotoPath: r.foto_path,
};
}
@@ -111,12 +107,11 @@ export function aRigaProfilo(p: Profilo): RigaProfilo {
documento_retro_path: oNull(p.documentoRetroPath),
certificato_scadenza: oNull(p.certificatoScadenza),
certificato_path: oNull(p.certificatoPath),
foto_path: oNull(p.fotoPath),
};
}
/** 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;
@@ -131,7 +126,6 @@ export function sezioniComplete(p: Profilo | null | undefined): Record<Sezione,
p.documentoRetroPath
),
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,
* certificato né foto tessera, che servono al tesseramento dei giocatori.
* L'allenatore compila solo i dati personali ridotti (DD-034): niente indirizzo, documento
* né certificato, che servono al tesseramento dei giocatori.
*/
export const CAMPI_ALLENATORE = ["dataNascita", "luogoNascita", "telefono", "email"] as const;
+1 -1
View File
@@ -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 TIPI_AMMESSI = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
-7
View File
@@ -8,7 +8,6 @@ import {
Download,
FileText,
IdCard,
Image,
Loader2,
Lock,
Send,
@@ -487,12 +486,6 @@ function SchedaGiocatore({
stato={certificato}
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
className={cn(
"flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold uppercase",
@@ -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;
+5 -5
View File
@@ -15,24 +15,24 @@ INSERT INTO public.profili_giocatore
(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)
certificato_scadenza, certificato_path)
VALUES
-- Profilo completo al 100%.
('g1', '1997-08-30', 'Bologna', 'Via Roma 1', '3330000001', 'g1@example.test',
'Carta d''identità', 'CA1000001', 'Comune di Bologna',
'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.
('g4', '1995-05-01', 'Bologna', 'Via Verdi 2', '3330000004', 'g4@example.test',
'Carta d''identità', 'CA1000004', 'Comune di Bologna',
'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',
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;
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
+6 -6
View File
@@ -24,12 +24,12 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
## Struttura
| 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 |
| `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 e M27, DD-040 e DD-042: palloni un solo avviso a 3 ore) 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ì |
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
| 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 |
| `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 e M27, DD-040 e DD-042: palloni un solo avviso a 3 ore) 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ì |
| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
## Server di sviluppo con Node vecchio
@@ -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");
}
+10 -6
View File
@@ -8,6 +8,7 @@ import {
completamento,
completamentoAllenatore,
csvTesseramento,
PESI,
daRigaProfilo,
formatDataBreve,
giorniAllaScadenza,
@@ -41,7 +42,6 @@ const vuoto: Profilo = {
documentoRetroPath: null,
certificatoScadenza: null,
certificatoPath: null,
fotoPath: null,
};
const completo: Profilo = {
@@ -60,18 +60,22 @@ const completo: Profilo = {
documentoRetroPath: "g1/documento-retro.jpg",
certificatoScadenza: "2027-06-30",
certificatoPath: "g1/certificato.pdf",
fotoPath: "g1/foto.jpg",
};
// --- completamento -----------------------------------------------------------
assert.equal(completamento(null), 0, "profilo inesistente = 0%");
assert.equal(completamento(vuoto), 0);
assert.equal(completamento(completo), 100, "tutte le sezioni piene = 100%");
assert.equal(completamento({ ...completo, fotoPath: null }), 90, "la foto pesa 10");
assert.equal(completamento({ ...completo, certificatoPath: null }), 70, "il certificato pesa 30");
assert.equal(
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(
completamento({ ...completo, email: null }),
70,
66,
"i dati personali sono completi solo tutti insieme",
);
@@ -216,7 +220,7 @@ assert.equal(
email: "a@b.it",
}),
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");
+2 -2
View File
@@ -14,11 +14,11 @@ await assert.rejects(
);
// --- 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",
});
await assert.rejects(
() => caricaFile("g1", "foto", troppoGrande),
() => caricaFile("g1", "certificato", troppoGrande),
/File troppo grande: massimo 8 MB\./,
);
+28
View File
@@ -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");