Dashboard amministratore e profilo per il tesseramento

Implementa la voce "Dashboard amministratore" della roadmap v1.1, come descritta
in docs/modules/profilo-giocatore.md, insieme alla parte di profilo che la
alimenta.

- /admin: stato dei profili della squadra, download di documento, certificato e
  foto tessera, export CSV con i 12 campi del tesseramento CSI. Un certificato
  scaduto non conta come valido.
- Profilo giocatore: dati personali, documento (fronte e retro), certificato e
  foto tessera, con widget di completamento in Home che sparisce al 100%.
- I permessi di amministrazione arrivano da user_roles (DD-011) e non più dalla
  lista di nomi in crapp-data.ts, che resta come ponte finché
  VITE_AUTH_OBBLIGATORIA non viene acceso in produzione.
- Login Google via Supabase Auth: al primo accesso l'account si collega a uno
  slot libero di giocatori_squadra, e il vincolo lo fa rispettare il trigger di
  M1 (DD-016 regola 2).

Migration additive: M2 crea profili_giocatore, M3 il bucket privato
profili-giocatore. Nessuna tabella v1.0 viene toccata, quindi si possono
applicare senza cambiare il comportamento attuale dell'app.

supabase/config.toml e seed.sql configurano lo stack locale: serve perché il
progetto Supabase è uno solo, condiviso tra sviluppo e produzione.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 17:57:59 +02:00
co-authored by Claude Opus 5
parent ea29f053e3
commit da51517ffc
23 changed files with 1635 additions and 63 deletions
+22 -14
View File
@@ -4,6 +4,7 @@ import { EventoCard, linkPerEvento } from "@/components/crapp/EventoCard";
import { PromemoriaPalloni } from "@/components/crapp/PromemoriaPalloni";
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
import { Section, StatTile, TeamLogo } from "@/components/crapp/ui-bits";
import { CompletaProfilo } from "@/components/crapp/ProfiloAmministrativo";
import { Reveal } from "@/components/motion/Reveal";
import { Barra } from "@/components/motion/Barra";
import { Numero } from "@/components/motion/Numero";
@@ -24,7 +25,8 @@ export const Route = createFileRoute("/")({
{ property: "og:title", content: "CrAPP — L'app del CRAP Volley" },
{
property: "og:description",
content: "Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
content:
"Convocazioni, presenze, statistiche e classifica del CRAP Volley in un'unica app mobile.",
},
],
}),
@@ -105,6 +107,8 @@ function Index() {
</div>
</Section>
<CompletaProfilo giocatoreId={giocatore.id} indice={2} />
<Section titolo="Da confermare" indice={2}>
<div className="space-y-3">
{prossimi.slice(1).map((e) => {
@@ -155,25 +159,29 @@ function Index() {
titolo="Obiettivo di squadra"
indice={4}
azione={
<Link to="/squadra" className="inline-flex items-center text-xs font-semibold text-accent">
<Link
to="/squadra"
className="inline-flex items-center text-xs font-semibold text-accent"
>
Tutti <ChevronRight className="h-3 w-3" />
</Link>
}
>
{obiettivo ? (
<div className="premi rounded-3xl bg-card p-4 shadow-card">
<div className="flex items-center gap-2 text-sm font-bold">
<span className="text-base leading-none">{obiettivo.emoji}</span> {obiettivo.titolo}
<div className="premi rounded-3xl bg-card p-4 shadow-card">
<div className="flex items-center gap-2 text-sm font-bold">
<span className="text-base leading-none">{obiettivo.emoji}</span> {obiettivo.titolo}
</div>
<Barra percentuale={progressoObiettivo(obiettivo)} trackClassName="mt-3" />
<p className="mt-2 text-xs text-muted-foreground">
Siamo al <Numero valore={progressoObiettivo(obiettivo)} suffisso="%" /> {" "}
{obiettivo.valore}/{obiettivo.target} {obiettivo.unita}.
</p>
<p className="mt-1 text-xs font-semibold text-accent">
{microcopyObiettivo(obiettivo)}
</p>
<p className="mt-1 text-[11px] text-muted-foreground">{obiettivo.impatto}</p>
</div>
<Barra percentuale={progressoObiettivo(obiettivo)} trackClassName="mt-3" />
<p className="mt-2 text-xs text-muted-foreground">
Siamo al <Numero valore={progressoObiettivo(obiettivo)} suffisso="%" /> {" "}
{obiettivo.valore}/{obiettivo.target}{" "}
{obiettivo.unita}.
</p>
<p className="mt-1 text-xs font-semibold text-accent">{microcopyObiettivo(obiettivo)}</p>
<p className="mt-1 text-[11px] text-muted-foreground">{obiettivo.impatto}</p>
</div>
) : null}
</Section>