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
+44
View File
@@ -0,0 +1,44 @@
import { useQuery } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { isAdmin as nomeInListaAdmin } from "./crapp-data";
import { useSessione } from "./auth";
import { useGiocatoreBase } from "./user-store";
export const RUOLI_KEY = ["ruolo-admin"] as const;
/**
* Permessi di amministrazione. La fonte è `user_roles` nel database (DD-011): la lista di
* nomi in `crapp-data.ts` resta solo come ponte per chi non ha ancora collegato l'account,
* e sparisce quando `VITE_AUTH_OBBLIGATORIA` viene acceso in produzione.
*
* ponytail: doppia fonte temporanea, si riduce a `ruoloDb` appena l'auth è obbligatoria.
*/
export function risolviAdmin(ruoloDb: boolean | null, giocatoreId: string | null): boolean {
if (ruoloDb !== null) return ruoloDb;
return giocatoreId ? nomeInListaAdmin(giocatoreId) : false;
}
/** `null` = nessuna sessione, quindi il database non ha una risposta da dare. */
async function fetchRuoloAdmin(utenteId: string | null): Promise<boolean | null> {
if (!utenteId) return null;
const { data, error } = await supabase
.from("user_roles")
.select("role")
.eq("user_id", utenteId)
.eq("role", "admin")
.maybeSingle();
if (error) throw error;
return !!data;
}
export function useIsAdmin(): boolean {
const { utenteId } = useSessione();
const io = useGiocatoreBase();
// Il ruolo cambia solo quando un admin lo assegna: una lettura per sessione basta.
const query = useQuery({
queryKey: [...RUOLI_KEY, utenteId],
queryFn: () => fetchRuoloAdmin(utenteId),
staleTime: 30 * 60_000,
});
return risolviAdmin(query.data ?? null, io?.id ?? null);
}