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:
1 parent
e822e4574b
commit
de78431090
21 files changed
+164
-79
No files matched your search
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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ì |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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"];
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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\./,
|
||||
);
|
||||
|
||||
|
||||
@@ -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");
|
||||
Reference in new issue
Block a user