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:
2026-09-01 10:46:54 +02:00
co-authored by Claude Mythos
parent 127e7c76e9
commit 215af4bbc4
11 changed files with 190 additions and 90 deletions
+19 -10
View File
@@ -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" };
+2 -11
View File
@@ -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" };