DD-018: collegamento automatico giocatore-account per email
Al primo accesso l'app collega da sola l'account Google al giocatore la cui email registrata coincide (case-insensitive), invece di far scegliere il nome da un elenco. Senza corrispondenza compare solo un messaggio d'errore con un pulsante per uscire e riprovare con un altro account: nessuna scelta manuale di ripiego. - Migration m5: colonna `email` su giocatori_squadra, seed per Ivan Cacciari e Davide Grilli, trigger esteso per richiedere anche il match email oltre allo slot libero. - slotPerEmail() sostituisce slotLiberi() (rimossa, senza più chiamanti in produzione). Co-Authored-By: Claude Mythos <noreply@anthropic.com>
This commit is contained in:
+9
-3
@@ -36,7 +36,9 @@ admin) implementati su `develop`, da attivare in produzione seguendo i passaggi
|
||||
## Database
|
||||
|
||||
- Schema v1.0 + M1 applicati al nuovo Supabase
|
||||
- `public.giocatori_squadra`: 17 giocatori iniziali presenti
|
||||
- `public.giocatori_squadra`: 17 giocatori iniziali presenti; solo 2 hanno l'email
|
||||
registrata (`email`, migration `m5_email_giocatori_squadra`, DD-018) — le altre 15
|
||||
arriveranno con una migration futura
|
||||
|
||||
---
|
||||
|
||||
@@ -86,8 +88,12 @@ Passaggi in ordine, nessuno dei quali è reversibile a metà:
|
||||
produzione senza toccare il comportamento attuale.
|
||||
3. **Primo admin**, dopo il primo login (l'ID esiste solo da quel momento):
|
||||
`INSERT INTO public.user_roles (user_id, role) SELECT id, 'admin' FROM auth.users WHERE email = '<mail>';`
|
||||
4. **Collegamento dei 17 account**: ciascuno accede con Google e sceglie il proprio nome una
|
||||
volta sola. Uno slot già collegato può essere liberato solo da un admin.
|
||||
4. **Collegamento dei 17 account**: ciascuno accede con Google e viene collegato in
|
||||
automatico al proprio giocatore per email (DD-018, migration
|
||||
`m5_email_giocatori_squadra`) — nessuna scelta manuale. Finché l'email di un giocatore
|
||||
non è impostata (oggi solo 2 dei 17 la hanno), il suo accesso mostra un errore e va
|
||||
sbloccato aggiungendo l'email con una nuova migration. Uno slot già collegato può essere
|
||||
liberato solo da un admin.
|
||||
5. **Solo a squadra collegata**: migration `m4_solo_autenticati`, che toglie al ruolo `anon`
|
||||
l'accesso alle tabelle v1.0. Da lì in poi i dati sono raggiungibili solo con una sessione;
|
||||
le route in `src/routes/api/public/` usano la service role e continuano a funzionare.
|
||||
|
||||
@@ -18,6 +18,11 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
|
||||
di nomi in `crapp-data.ts` è stata eliminata, altrimenti bastava scegliere il nome giusto
|
||||
per amministrare.
|
||||
- Migration `m4_solo_autenticati`: toglie al ruolo `anon` l'accesso alle tabelle v1.0.
|
||||
- Collegamento automatico al proprio giocatore per email (DD-018, migration
|
||||
`m5_email_giocatori_squadra`): niente più scelta manuale da un elenco, `/benvenuto`
|
||||
confronta l'email dell'account Google con `giocatori_squadra.email` e collega da solo.
|
||||
Senza corrispondenza compare solo un messaggio d'errore, con un pulsante per uscire e
|
||||
riprovare con un altro account.
|
||||
**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
|
||||
|
||||
+1
-1
@@ -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) e collegamento all'account (`auth_user_id`). | 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. |
|
||||
| `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` | 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). |
|
||||
|
||||
@@ -33,6 +33,7 @@ Serve a rispondere a domande del tipo:
|
||||
| [DD-013](#dd-013--portabilità-lapp-non-deve-dipendere-da-servizi-esclusivi) | Portabilità dello stack |
|
||||
| [DD-016](#dd-016--schema-dati-profilo-giocatore-v11-f0) | Schema dati Profilo Giocatore v1.1 |
|
||||
| [DD-017](#dd-017--lamministratore-può-compilare-i-dati-al-posto-del-giocatore) | L'admin scrive al posto del giocatore |
|
||||
| [DD-018](#dd-018--collegamento-automatico-giocatoreaccount-per-email) | Collegamento automatico per email |
|
||||
|
||||
**In valutazione**
|
||||
|
||||
@@ -474,6 +475,32 @@ Restano fuori, e non cambiano:
|
||||
|
||||
---
|
||||
|
||||
### DD-018 — Collegamento automatico giocatore↔account per email
|
||||
|
||||
**Data:** settembre 2026
|
||||
**Stato:** Accettata
|
||||
|
||||
**Contesto**
|
||||
DD-016 regola 2 prevedeva che, al primo accesso, il giocatore scegliesse manualmente il proprio slot libero da un elenco (`/benvenuto`). In pratica ogni giocatore ha un'email nota (o presto nota), quindi far scegliere un nome da una lista è un passaggio superfluo e un rischio: un giocatore può selezionare per errore lo slot di un compagno, e nulla nel flusso lo impedisce a livello di prodotto.
|
||||
|
||||
**Decisione**
|
||||
Al primo accesso, `giocatori_squadra` viene interrogata per email (case-insensitive, tramite la nuova colonna `email`) invece di mostrare un elenco di slot liberi. Se l'email dell'account Google corrisponde a una riga libera, il collegamento avviene automaticamente. Se non corrisponde a nessuna riga (email non ancora nota, o nessun profilo per quella persona), l'utente vede solo un messaggio d'errore che invita a contattare un amministratore, con un pulsante per uscire e riprovare con un altro account — nessuna selezione manuale di ripiego. Le email sono popolate via migration (`m5_email_giocatori_squadra`), non tramite un'interfaccia amministrativa in questa iterazione. Il trigger `enforce_giocatori_squadra_update` (DD-016) viene esteso per richiedere anche la corrispondenza email, non solo lo slot libero: il vincolo resta nel database, non solo nella UI.
|
||||
|
||||
**Alternative scartate**
|
||||
- Mantenere la selezione manuale come ripiego quando l'email non trova corrispondenza → scartata: vanificherebbe la garanzia "ognuno collega solo il proprio profilo" e reintrodurrebbe il rischio di scelta errata che questa decisione vuole eliminare.
|
||||
- Un'interfaccia admin per scrivere l'email dei giocatori → rimandata: non necessaria finché le email arrivano da migration; si può aggiungere in futuro senza toccare questa decisione.
|
||||
|
||||
**Conseguenze**
|
||||
- Le righe senza email nota restano bloccate — nessuno può collegarle, nemmeno per errore — finché una migration futura non la imposta. Oggi solo 2 dei 17 giocatori hanno l'email registrata.
|
||||
- `slotLiberi` (funzione ed elenco "slot liberi" in `/benvenuto`) è stato rimosso: non aveva più chiamanti in produzione dopo il cambio.
|
||||
- Un utente che accede con l'account Google sbagliato resta bloccato su `/benvenuto` finché non esce e riprova con l'account giusto.
|
||||
|
||||
**Riesame**
|
||||
- Quando tutte le 17 email saranno note e verificate.
|
||||
- Se in futuro serve un'assistenza admin diretta dal flusso di login invece che da `/admin`.
|
||||
|
||||
---
|
||||
|
||||
## Decisioni in valutazione
|
||||
|
||||
---
|
||||
|
||||
@@ -40,7 +40,10 @@ responsabilità del giocatore che li fornisce.
|
||||
1. Login tramite Google oppure Email. *Implementato con il solo Google: la squadra ha tutti
|
||||
un account Google, e un secondo metodo è additivo (un bottone in più sulla stessa
|
||||
schermata) il giorno che serve.*
|
||||
2. Selezione del proprio giocatore.
|
||||
2. Collegamento automatico al proprio giocatore, confrontando l'email dell'account Google
|
||||
con l'email registrata in `giocatori_squadra` (DD-018). Nessuna scelta manuale: se
|
||||
l'email non corrisponde a nessun profilo, l'accesso si ferma con un messaggio che invita
|
||||
a contattare un amministratore.
|
||||
3. Accesso alla Home.
|
||||
|
||||
Se il profilo non è completo compare automaticamente un widget di completamento.
|
||||
|
||||
+6
-1
@@ -39,7 +39,12 @@ export function useSessione() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { sessione, pronta, utenteId: sessione?.user.id ?? null };
|
||||
return {
|
||||
sessione,
|
||||
pronta,
|
||||
utenteId: sessione?.user.id ?? null,
|
||||
emailUtente: sessione?.user.email ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function accediConGoogle(): Promise<void> {
|
||||
|
||||
@@ -15,6 +15,7 @@ export type GiocatoreSquadra = {
|
||||
ruolo: string;
|
||||
authUserId: string | null;
|
||||
attivo: boolean;
|
||||
email: string | null;
|
||||
};
|
||||
|
||||
type RigaGiocatoreSquadra = {
|
||||
@@ -25,6 +26,7 @@ type RigaGiocatoreSquadra = {
|
||||
ruolo: string;
|
||||
auth_user_id: string | null;
|
||||
attivo: boolean;
|
||||
email: string | null;
|
||||
};
|
||||
|
||||
export const SQUADRA_KEY = ["giocatori-squadra"] as const;
|
||||
@@ -45,6 +47,7 @@ export function rosaFallback(): GiocatoreSquadra[] {
|
||||
ruolo: g.ruolo,
|
||||
authUserId: null,
|
||||
attivo: true,
|
||||
email: null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -61,14 +64,20 @@ export function slotDi(
|
||||
return righe.find((g) => g.authUserId === utenteId) ?? null;
|
||||
}
|
||||
|
||||
export function slotLiberi(righe: GiocatoreSquadra[]): GiocatoreSquadra[] {
|
||||
return righe.filter((g) => g.attivo && !g.authUserId);
|
||||
/** Lo slot libero la cui email coincide con quella dell'account Google (case-insensitive). */
|
||||
export function slotPerEmail(
|
||||
righe: GiocatoreSquadra[],
|
||||
email: string | null,
|
||||
): GiocatoreSquadra | null {
|
||||
if (!email) return null;
|
||||
const cercata = email.trim().toLowerCase();
|
||||
return righe.find((g) => !g.authUserId && g.email?.trim().toLowerCase() === cercata) ?? null;
|
||||
}
|
||||
|
||||
async function fetchSquadra(): Promise<GiocatoreSquadra[]> {
|
||||
const { data, error } = await supabaseNuoveTabelle
|
||||
.from("giocatori_squadra")
|
||||
.select("id, nome, cognome, numero, ruolo, auth_user_id, attivo")
|
||||
.select("id, nome, cognome, numero, ruolo, auth_user_id, attivo, email")
|
||||
.order("id");
|
||||
if (error) throw error;
|
||||
const righe = (data ?? []) as RigaGiocatoreSquadra[];
|
||||
@@ -80,6 +89,7 @@ async function fetchSquadra(): Promise<GiocatoreSquadra[]> {
|
||||
ruolo: r.ruolo,
|
||||
authUserId: r.auth_user_id,
|
||||
attivo: r.attivo,
|
||||
email: r.email,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+42
-60
@@ -1,16 +1,14 @@
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LogIn } from "lucide-react";
|
||||
import { LogIn, LogOut } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { TeamLogo } from "@/components/crapp/ui-bits";
|
||||
import { accediConGoogle, useSessione } from "@/lib/auth";
|
||||
import { accediConGoogle, esci, useSessione } from "@/lib/auth";
|
||||
import {
|
||||
nomeCompleto,
|
||||
slotDi,
|
||||
slotLiberi,
|
||||
slotPerEmail,
|
||||
useCollegaGiocatore,
|
||||
useGiocatoriSquadra,
|
||||
type GiocatoreSquadra,
|
||||
} from "@/lib/giocatori-squadra";
|
||||
import { impostaGiocatore, resetGiocatore, useGiocatoreCorrente } from "@/lib/user-store";
|
||||
|
||||
@@ -32,43 +30,17 @@ export const Route = createFileRoute("/benvenuto")({
|
||||
component: Benvenuto,
|
||||
});
|
||||
|
||||
function Scheda({
|
||||
titolo,
|
||||
sottotitolo,
|
||||
onClick,
|
||||
iniziali,
|
||||
}: {
|
||||
titolo: string;
|
||||
sottotitolo: string;
|
||||
onClick: () => void;
|
||||
iniziali: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex w-full items-center gap-4 rounded-2xl bg-card p-4 shadow-card transition-transform active:scale-[0.98]"
|
||||
>
|
||||
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-xl bg-secondary font-display text-lg">
|
||||
{iniziali}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<p className="font-semibold leading-tight">{titolo}</p>
|
||||
<p className="text-xs text-muted-foreground">{sottotitolo}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Benvenuto() {
|
||||
const navigate = useNavigate();
|
||||
const giocatore = useGiocatoreCorrente();
|
||||
const { pronta, utenteId } = useSessione();
|
||||
const { pronta, utenteId, emailUtente } = useSessione();
|
||||
const { righe, daDatabase } = useGiocatoriSquadra();
|
||||
const collega = useCollegaGiocatore();
|
||||
const [inCorso, setInCorso] = useState(false);
|
||||
const [tentato, setTentato] = useState(false);
|
||||
|
||||
const mioSlot = slotDi(righe, utenteId);
|
||||
const slotEmail = slotPerEmail(righe, emailUtente);
|
||||
// Si entra solo da loggati e con uno slot collegato (DD-011).
|
||||
const puoEntrare = !!giocatore && !!utenteId;
|
||||
|
||||
@@ -83,6 +55,18 @@ function Benvenuto() {
|
||||
else if (utenteId && daDatabase) resetGiocatore();
|
||||
}, [mioSlot, utenteId, daDatabase]);
|
||||
|
||||
// Collegamento automatico per email (DD-018): un solo tentativo, mai su dati di
|
||||
// fallback. Se fallisce o non trova corrispondenza resta lo stato d'errore, senza
|
||||
// scelta manuale di ripiego.
|
||||
useEffect(() => {
|
||||
if (!utenteId || !daDatabase || mioSlot || !slotEmail || tentato) return;
|
||||
setTentato(true);
|
||||
collega
|
||||
.mutateAsync({ giocatoreId: slotEmail.id, utenteId })
|
||||
.then(() => impostaGiocatore(slotEmail.id))
|
||||
.catch(() => toast.error("Collegamento non riuscito. Riprova o contatta un amministratore."));
|
||||
}, [utenteId, daDatabase, mioSlot, slotEmail, tentato, collega]);
|
||||
|
||||
async function accedi() {
|
||||
setInCorso(true);
|
||||
try {
|
||||
@@ -93,17 +77,19 @@ function Benvenuto() {
|
||||
}
|
||||
}
|
||||
|
||||
async function reclama(g: GiocatoreSquadra) {
|
||||
if (!utenteId) return;
|
||||
async function esciERiprova() {
|
||||
try {
|
||||
await collega.mutateAsync({ giocatoreId: g.id, utenteId });
|
||||
impostaGiocatore(g.id);
|
||||
} catch {
|
||||
toast.error("Profilo già collegato a un altro account. Chiedi a un amministratore.");
|
||||
await esci();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Uscita non riuscita");
|
||||
}
|
||||
}
|
||||
|
||||
const liberi = slotLiberi(righe);
|
||||
// Errore: email senza corrispondenza, oppure trovata ma il tentativo di collegamento
|
||||
// è fallito (slot nel frattempo reclamato da altri, o errore di rete).
|
||||
const erroreCollegamento =
|
||||
!!utenteId && daDatabase && !mioSlot && (!slotEmail || collega.isError);
|
||||
const inAttesaCollegamento = !!utenteId && !mioSlot && !erroreCollegamento;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center px-6 py-12">
|
||||
@@ -126,29 +112,25 @@ function Benvenuto() {
|
||||
<LogIn className="h-4 w-4" /> Accedi con Google
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
) : erroreCollegamento ? (
|
||||
<>
|
||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
||||
Sei entrato. Scegli il tuo nome: resterà collegato a questo account.
|
||||
Nessun profilo trovato per la tua email. Contatta un amministratore per collegare il tuo
|
||||
account.
|
||||
</p>
|
||||
<div className="mt-8 w-full max-w-sm space-y-2">
|
||||
{liberi.map((g) => (
|
||||
<Scheda
|
||||
key={g.id}
|
||||
titolo={nomeCompleto(g)}
|
||||
sottotitolo={`#${g.numero} · ${g.ruolo}`}
|
||||
iniziali={`${g.nome[0] ?? ""}${g.cognome[0] ?? ""}`.toUpperCase()}
|
||||
onClick={() => void reclama(g)}
|
||||
/>
|
||||
))}
|
||||
{liberi.length === 0 ? (
|
||||
<p className="rounded-2xl bg-card p-4 text-center text-sm text-muted-foreground shadow-card">
|
||||
Nessun profilo libero: chiedi a un amministratore di collegarti.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void esciERiprova()}
|
||||
className="premi mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-secondary py-3.5 text-sm font-bold uppercase shadow-card"
|
||||
>
|
||||
<LogOut className="h-4 w-4" /> Esci
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
) : inAttesaCollegamento ? (
|
||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
||||
Ti stiamo collegando al tuo profilo...
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
-- M5 — Collegamento automatico per email al primo accesso (DD-018)
|
||||
--
|
||||
-- Sostituisce la scelta manuale del proprio slot in `/benvenuto` con un confronto tra
|
||||
-- l'email dell'account Google e l'email registrata per ciascun giocatore. Additiva:
|
||||
-- non tocca `profili_giocatore` né altre tabelle v1.0.
|
||||
|
||||
ALTER TABLE public.giocatori_squadra
|
||||
ADD COLUMN email text;
|
||||
|
||||
COMMENT ON COLUMN public.giocatori_squadra.email IS
|
||||
'Email nota in anticipo per il giocatore (non l''email personale in profili_giocatore): '
|
||||
'chiave del collegamento automatico account <-> giocatore al primo accesso (DD-018). '
|
||||
'NULL finché non nota: la riga resta non collegabile da nessuno.';
|
||||
|
||||
-- Case-insensitive, tollerante a più righe con email NULL (non ancora note).
|
||||
CREATE UNIQUE INDEX giocatori_squadra_email_unica
|
||||
ON public.giocatori_squadra (lower(email))
|
||||
WHERE email IS NOT NULL;
|
||||
|
||||
-- Il trigger di DD-016 (m1_giocatori_squadra) blocca qualunque UPDATE che non arrivi da un
|
||||
-- admin autenticato o dal giocatore stesso: durante la migration non c'è sessione, quindi
|
||||
-- va disattivato solo per il seed e riattivato subito dopo.
|
||||
ALTER TABLE public.giocatori_squadra DISABLE TRIGGER enforce_giocatori_squadra_update;
|
||||
|
||||
-- Solo le due email confermate finora; le altre 15 arriveranno con una migration futura.
|
||||
UPDATE public.giocatori_squadra SET email = 'ivan.cacciari.1995@gmail.com' WHERE id = 'g4';
|
||||
UPDATE public.giocatori_squadra SET email = 'davide3011@gmail.com' WHERE id = 'g10';
|
||||
|
||||
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
|
||||
|
||||
-- Estende il trigger di DD-016: oltre a "slot libero, nessun altro campo cambia", un
|
||||
-- non-admin può collegarsi solo se l'email del token coincide con l'email della riga
|
||||
-- (case-insensitive). Righe senza email restano bloccate per chiunque.
|
||||
CREATE OR REPLACE FUNCTION public.enforce_giocatori_squadra_update()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
IF public.has_role(auth.uid(), 'admin'::public.app_role) THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF OLD.auth_user_id IS NULL
|
||||
AND NEW.auth_user_id = auth.uid()
|
||||
AND OLD.email IS NOT NULL
|
||||
AND lower(OLD.email) = lower(coalesce(auth.jwt() ->> 'email', ''))
|
||||
AND NEW.id IS NOT DISTINCT FROM OLD.id
|
||||
AND NEW.nome IS NOT DISTINCT FROM OLD.nome
|
||||
AND NEW.cognome IS NOT DISTINCT FROM OLD.cognome
|
||||
AND NEW.numero IS NOT DISTINCT FROM OLD.numero
|
||||
AND NEW.ruolo IS NOT DISTINCT FROM OLD.ruolo
|
||||
AND NEW.attivo IS NOT DISTINCT FROM OLD.attivo
|
||||
AND NEW.email IS NOT DISTINCT FROM OLD.email
|
||||
AND NEW.creato_il IS NOT DISTINCT FROM OLD.creato_il THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION 'Aggiornamento non autorizzato su giocatori_squadra';
|
||||
END;
|
||||
$$;
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
numeroGiaUsato,
|
||||
rosaFallback,
|
||||
slotDi,
|
||||
slotLiberi,
|
||||
slotPerEmail,
|
||||
validaDatiSquadra,
|
||||
type GiocatoreSquadra,
|
||||
} from "@/lib/giocatori-squadra";
|
||||
@@ -20,6 +20,7 @@ const riga = (parziale: Partial<GiocatoreSquadra> = {}): GiocatoreSquadra => ({
|
||||
ruolo: "schiacciatore",
|
||||
authUserId: null,
|
||||
attivo: true,
|
||||
email: null,
|
||||
...parziale,
|
||||
});
|
||||
|
||||
@@ -62,17 +63,25 @@ assert.equal(slotDi(righe, "u1")?.id, "g1");
|
||||
assert.equal(slotDi(righe, "u9"), null, "nessuno slot per un account non collegato");
|
||||
assert.equal(slotDi(righe, null), null, "senza sessione non c'è slot");
|
||||
|
||||
// --- slotLiberi ----------------------------------------------------------------
|
||||
const rosaMista: GiocatoreSquadra[] = [
|
||||
riga({ id: "g1", attivo: true, authUserId: null }),
|
||||
riga({ id: "g2", attivo: true, authUserId: "u1" }),
|
||||
riga({ id: "g3", attivo: false, authUserId: null }),
|
||||
// --- slotPerEmail ----------------------------------------------------------------
|
||||
const rosaEmail: GiocatoreSquadra[] = [
|
||||
riga({ id: "g1", email: "foo@bar.com", authUserId: null }),
|
||||
riga({ id: "g2", email: "baz@qux.com", authUserId: "u1" }),
|
||||
riga({ id: "g3", email: null, authUserId: null }),
|
||||
];
|
||||
assert.deepEqual(
|
||||
slotLiberi(rosaMista).map((g) => g.id),
|
||||
["g1"],
|
||||
"libero solo chi è attivo e senza account collegato",
|
||||
assert.equal(slotPerEmail(rosaEmail, "foo@bar.com")?.id, "g1", "match esatto");
|
||||
assert.equal(
|
||||
slotPerEmail(rosaEmail, "Foo@Bar.com")?.id,
|
||||
"g1",
|
||||
"il confronto ignora maiuscole/minuscole",
|
||||
);
|
||||
assert.equal(slotPerEmail(rosaEmail, null), null, "senza email non c'è match");
|
||||
assert.equal(
|
||||
slotPerEmail(rosaEmail, "baz@qux.com"),
|
||||
null,
|
||||
"uno slot già collegato non risulta in match anche con email coincidente",
|
||||
);
|
||||
assert.equal(slotPerEmail(rosaEmail, ""), null, "stringa vuota trattata come nessuna email");
|
||||
|
||||
// --- validaDatiSquadra ----------------------------------------------------------------
|
||||
const datiOk = { nome: "Mario", cognome: "Rossi", numero: 7, ruolo: "schiacciatore" };
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
numeroGiaUsato,
|
||||
rosaFallback,
|
||||
slotDi,
|
||||
slotLiberi,
|
||||
validaDatiSquadra,
|
||||
type GiocatoreSquadra,
|
||||
} from "@/lib/giocatori-squadra";
|
||||
@@ -112,6 +111,7 @@ const squadra: GiocatoreSquadra[] = [
|
||||
ruolo: "Banda",
|
||||
authUserId: null,
|
||||
attivo: true,
|
||||
email: null,
|
||||
},
|
||||
{
|
||||
id: "g2",
|
||||
@@ -121,6 +121,7 @@ const squadra: GiocatoreSquadra[] = [
|
||||
ruolo: "Libero",
|
||||
authUserId: "u2",
|
||||
attivo: true,
|
||||
email: null,
|
||||
},
|
||||
];
|
||||
const csv = csvTesseramento(squadra, { g1: completo });
|
||||
@@ -146,16 +147,6 @@ assert.deepEqual(dividiNome("Ivan"), { nome: "Ivan", cognome: "" });
|
||||
assert.equal(slotDi(squadra, null), null, "senza sessione nessuno slot");
|
||||
assert.equal(slotDi(squadra, "u2")?.id, "g2");
|
||||
assert.equal(slotDi(squadra, "sconosciuto"), null);
|
||||
assert.deepEqual(
|
||||
slotLiberi(squadra).map((g) => g.id),
|
||||
["g1"],
|
||||
"uno slot già collegato non è più libero",
|
||||
);
|
||||
assert.deepEqual(
|
||||
slotLiberi([...squadra, { ...squadra[0]!, id: "g3", attivo: false }]).map((g) => g.id),
|
||||
["g1"],
|
||||
"i giocatori non attivi restano fuori",
|
||||
);
|
||||
|
||||
// --- dati squadra modificabili dall'admin (DD-017) ---------------------------
|
||||
const datiOk = { nome: "Ivan", cognome: "Cacciari", numero: 23, ruolo: "Banda" };
|
||||
|
||||
Reference in New Issue
Block a user