Dashboard admin: aggiungi, disattiva e riattiva giocatori
- Aggiungi giocatore: nuovo form in /admin (nome, cognome, numero, ruolo, email opzionale), id g<N> calcolato in automatico. - Email modificabile anche per i giocatori già in rosa dal pannello Dati squadra, non solo alla creazione — completa quanto rimandato da DD-018. - Disattiva/Riattiva: un giocatore che lascia la squadra sparisce dalla rosa attiva senza che la riga venga eliminata, così presenze, voti, pagelle e badge della stagione restano agganciati al suo id. Nuova sezione "Giocatori disattivati" per riattivarli. - Messaggio d'errore leggibile per l'unico vincolo unique della tabella (email duplicata) invece del codice Postgres grezzo. Nessuna migration: sia l'inserimento sia la modifica passano dalla policy admin FOR ALL già esistente su giocatori_squadra. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,11 @@ qui: sta in [ROADMAP.md](ROADMAP.md).
|
||||
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.
|
||||
- Dashboard admin: nuove azioni "Aggiungi giocatore" (con email opzionale per il
|
||||
collegamento automatico) e "Disattiva/Riattiva giocatore" per chi lascia la squadra — la
|
||||
riga non viene mai eliminata, così presenze, voti, pagelle e badge della stagione restano
|
||||
agganciati al suo id. L'email è anche modificabile dal pannello "Dati squadra" di ogni
|
||||
giocatore già in rosa.
|
||||
**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
|
||||
|
||||
@@ -26,9 +26,13 @@ Può:
|
||||
- scaricare documenti e certificati
|
||||
- esportare i dati necessari al tesseramento CSI
|
||||
- verificare lo stato di completamento dei profili
|
||||
- modificare i dati squadra di qualsiasi giocatore (nome, cognome, numero, ruolo)
|
||||
- modificare i dati squadra di qualsiasi giocatore (nome, cognome, numero, ruolo, email)
|
||||
- compilare e correggere i dati personali e del documento al posto di un giocatore (DD-017)
|
||||
- scollegare un account da un profilo, liberando lo slot
|
||||
- aggiungere un nuovo giocatore alla rosa (id, nome, cognome, numero, ruolo, email opzionale)
|
||||
- disattivare un giocatore che ha lasciato la squadra, e riattivarlo in caso di errore: la
|
||||
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
|
||||
responsabilità del giocatore che li fornisce.
|
||||
@@ -163,6 +167,8 @@ Azioni disponibili.
|
||||
- Scarica foto tessera
|
||||
- Modifica dati squadra e dati personali del giocatore (DD-017)
|
||||
- Scollega account, per liberare uno slot assegnato per errore
|
||||
- Aggiungi giocatore, per inserire un nuovo membro della squadra
|
||||
- Disattiva/Riattiva giocatore, per chi lascia la squadra (o rientra)
|
||||
|
||||
## Esportazione CSI
|
||||
|
||||
|
||||
@@ -100,8 +100,9 @@ export function useGiocatoriSquadra() {
|
||||
return { ...query, righe, daDatabase: !!query.data?.length };
|
||||
}
|
||||
|
||||
/** Dati squadra: li gestisce solo un amministratore (DD-017). */
|
||||
export type DatiSquadra = Pick<GiocatoreSquadra, "nome" | "cognome" | "numero" | "ruolo">;
|
||||
/** Dati squadra: li gestisce solo un amministratore (DD-017). L'email è quella usata per
|
||||
* il collegamento automatico al primo accesso (DD-018), non il dato personale del profilo. */
|
||||
export type DatiSquadra = Pick<GiocatoreSquadra, "nome" | "cognome" | "numero" | "ruolo" | "email">;
|
||||
|
||||
/**
|
||||
* Controlli che rispecchiano i vincoli della tabella (`numero > 0`, campi obbligatori):
|
||||
@@ -114,9 +115,19 @@ export function validaDatiSquadra(dati: DatiSquadra): string | null {
|
||||
if (!Number.isInteger(dati.numero) || dati.numero <= 0)
|
||||
return "Il numero di maglia deve essere maggiore di zero.";
|
||||
if (!dati.ruolo.trim()) return "Il ruolo non può essere vuoto.";
|
||||
if (dati.email?.trim() && !dati.email.includes("@")) return "L'email non è valida.";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Il prossimo id libero nel formato `g<N>` richiesto dal vincolo della tabella. */
|
||||
export function prossimoIdGiocatore(righe: GiocatoreSquadra[]): string {
|
||||
const max = righe.reduce((acc, g) => {
|
||||
const n = Number(g.id.slice(1));
|
||||
return Number.isFinite(n) && n > acc ? n : acc;
|
||||
}, 0);
|
||||
return `g${max + 1}`;
|
||||
}
|
||||
|
||||
/** Numeri di maglia doppi: il database li accetta, la squadra no. */
|
||||
export function numeroGiaUsato(
|
||||
righe: GiocatoreSquadra[],
|
||||
@@ -131,21 +142,71 @@ export function useSalvaDatiSquadra() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (input: { giocatoreId: string; dati: DatiSquadra }) => {
|
||||
const dati = {
|
||||
nome: input.dati.nome.trim(),
|
||||
cognome: input.dati.cognome.trim(),
|
||||
numero: input.dati.numero,
|
||||
ruolo: input.dati.ruolo.trim(),
|
||||
email: input.dati.email?.trim() || null,
|
||||
};
|
||||
const { error } = await supabaseNuoveTabelle
|
||||
.from("giocatori_squadra")
|
||||
.update({
|
||||
nome: input.dati.nome.trim(),
|
||||
cognome: input.dati.cognome.trim(),
|
||||
numero: input.dati.numero,
|
||||
ruolo: input.dati.ruolo.trim(),
|
||||
})
|
||||
.update(dati)
|
||||
.eq("id", input.giocatoreId);
|
||||
if (error) throw error;
|
||||
return { giocatoreId: input.giocatoreId, dati };
|
||||
},
|
||||
onSuccess: (input) => {
|
||||
queryClient.setQueryData<GiocatoreSquadra[]>(SQUADRA_KEY, (prec) =>
|
||||
(prec ?? []).map((g) => (g.id === input.giocatoreId ? { ...g, ...input.dati } : g)),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggiunge un giocatore alla rosa (DD-017). Solo un admin passa le policy di M1.
|
||||
* L'id (`g<N>`) non è generato dal database: va calcolato con `prossimoIdGiocatore`
|
||||
* prima di chiamare questa mutazione.
|
||||
*/
|
||||
export function useAggiungiGiocatore() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (input: { id: string; dati: DatiSquadra }) => {
|
||||
const { error } = await supabaseNuoveTabelle.from("giocatori_squadra").insert({
|
||||
id: input.id,
|
||||
nome: input.dati.nome.trim(),
|
||||
cognome: input.dati.cognome.trim(),
|
||||
numero: input.dati.numero,
|
||||
ruolo: input.dati.ruolo.trim(),
|
||||
email: input.dati.email?.trim() || null,
|
||||
});
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: SQUADRA_KEY });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attiva o disattiva un giocatore (es. ha lasciato la squadra): non elimina la riga, così
|
||||
* presenze, voti, pagelle e badge della stagione restano agganciati al suo id.
|
||||
*/
|
||||
export function useImpostaAttivo() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (input: { giocatoreId: string; attivo: boolean }) => {
|
||||
const { error } = await supabaseNuoveTabelle
|
||||
.from("giocatori_squadra")
|
||||
.update({ attivo: input.attivo })
|
||||
.eq("id", input.giocatoreId);
|
||||
if (error) throw error;
|
||||
return input;
|
||||
},
|
||||
onSuccess: (input) => {
|
||||
queryClient.setQueryData<GiocatoreSquadra[]>(SQUADRA_KEY, (prec) =>
|
||||
(prec ?? []).map((g) => (g.id === input.giocatoreId ? { ...g, ...input.dati } : g)),
|
||||
(prec ?? []).map((g) => (g.id === input.giocatoreId ? { ...g, attivo: input.attivo } : g)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
+224
-7
@@ -9,6 +9,9 @@ import {
|
||||
Loader2,
|
||||
Lock,
|
||||
Unlink,
|
||||
UserCheck,
|
||||
UserPlus,
|
||||
UserX,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -17,7 +20,10 @@ import { CampiProfilo } from "@/components/crapp/ProfiloAmministrativo";
|
||||
import {
|
||||
nomeCompleto,
|
||||
numeroGiaUsato,
|
||||
prossimoIdGiocatore,
|
||||
useAggiungiGiocatore,
|
||||
useGiocatoriSquadra,
|
||||
useImpostaAttivo,
|
||||
useSalvaDatiSquadra,
|
||||
useScollegaAccount,
|
||||
validaDatiSquadra,
|
||||
@@ -68,6 +74,15 @@ const statoClasse: Record<StatoScadenza | "presente" | "assente", string> = {
|
||||
|
||||
const classiInput = "w-full rounded-xl border border-border bg-background px-3 py-2 text-sm";
|
||||
|
||||
/** L'unico vincolo unique lato database sulla tabella è l'email: messaggio leggibile invece
|
||||
* del codice Postgres (23505). */
|
||||
function messaggioErrore(e: unknown, fallback: string): string {
|
||||
if (e && typeof e === "object" && "code" in e && (e as { code?: unknown }).code === "23505") {
|
||||
return "Email già usata da un altro giocatore.";
|
||||
}
|
||||
return e instanceof Error ? e.message : fallback;
|
||||
}
|
||||
|
||||
function Campo({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<label className="block">
|
||||
@@ -88,6 +103,7 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
||||
const salvaSquadra = useSalvaDatiSquadra();
|
||||
const salvaProfilo = useSalvaProfilo();
|
||||
const scollega = useScollegaAccount();
|
||||
const impostaAttivo = useImpostaAttivo();
|
||||
|
||||
const [datiSquadra, setDatiSquadra] = useState<DatiSquadra | null>(null);
|
||||
const [bozza, setBozza] = useState<Profilo | null>(null);
|
||||
@@ -97,6 +113,7 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
||||
cognome: g.cognome,
|
||||
numero: g.numero,
|
||||
ruolo: g.ruolo,
|
||||
email: g.email,
|
||||
};
|
||||
const profiloCorrente = bozza ?? profilo ?? profiloVuoto(g.id);
|
||||
|
||||
@@ -114,7 +131,20 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
||||
setDatiSquadra(null);
|
||||
toast.success("Dati squadra aggiornati");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Salvataggio non riuscito");
|
||||
toast.error(messaggioErrore(e, "Salvataggio non riuscito"));
|
||||
}
|
||||
}
|
||||
|
||||
async function confermaAttivo(nuovo: boolean) {
|
||||
const messaggio = nuovo
|
||||
? `Riattivare ${nomeCompleto(g)}?`
|
||||
: `Disattivare ${nomeCompleto(g)}? Sparisce dalla rosa attiva ma i suoi dati (presenze, voti, badge) restano.`;
|
||||
if (!confirm(messaggio)) return;
|
||||
try {
|
||||
await impostaAttivo.mutateAsync({ giocatoreId: g.id, attivo: nuovo });
|
||||
toast.success(nuovo ? "Giocatore riattivato" : "Giocatore disattivato");
|
||||
} catch (e) {
|
||||
toast.error(messaggioErrore(e, "Operazione non riuscita"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +211,15 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
||||
/>
|
||||
</Campo>
|
||||
</div>
|
||||
<Campo label="Email (collegamento automatico al login, DD-018)">
|
||||
<input
|
||||
type="email"
|
||||
value={squadraCorrente.email ?? ""}
|
||||
placeholder="nome@esempio.com"
|
||||
onChange={(e) => setDatiSquadra({ ...squadraCorrente, email: e.target.value || null })}
|
||||
className={classiInput}
|
||||
/>
|
||||
</Campo>
|
||||
<button
|
||||
type="button"
|
||||
onClick={confermaSquadra}
|
||||
@@ -208,16 +247,37 @@ function ModificaGiocatore({ g, profilo }: { g: GiocatoreSquadra; profilo: Profi
|
||||
<span className="min-w-0 text-muted-foreground">
|
||||
{g.authUserId ? "Account collegato" : "Nessun account collegato"}
|
||||
</span>
|
||||
{g.authUserId ? (
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{g.authUserId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={confermaScollega}
|
||||
disabled={scollega.isPending}
|
||||
className="premi flex items-center gap-1.5 rounded-xl bg-destructive px-3 py-2 font-bold text-destructive-foreground disabled:opacity-50"
|
||||
>
|
||||
<Unlink className="h-3.5 w-3.5" /> Scollega
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={confermaScollega}
|
||||
disabled={scollega.isPending}
|
||||
className="premi flex shrink-0 items-center gap-1.5 rounded-xl bg-destructive px-3 py-2 font-bold text-destructive-foreground disabled:opacity-50"
|
||||
onClick={() => void confermaAttivo(!g.attivo)}
|
||||
disabled={impostaAttivo.isPending}
|
||||
className={cn(
|
||||
"premi flex items-center gap-1.5 rounded-xl px-3 py-2 font-bold disabled:opacity-50",
|
||||
g.attivo ? "bg-secondary text-foreground" : "bg-success text-success-foreground",
|
||||
)}
|
||||
>
|
||||
<Unlink className="h-3.5 w-3.5" /> Scollega
|
||||
{g.attivo ? (
|
||||
<>
|
||||
<UserX className="h-3.5 w-3.5" /> Disattiva
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserCheck className="h-3.5 w-3.5" /> Riattiva
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -348,6 +408,151 @@ function SchedaGiocatore({
|
||||
);
|
||||
}
|
||||
|
||||
const datiVuoti: DatiSquadra = { nome: "", cognome: "", numero: 1, ruolo: "", email: null };
|
||||
|
||||
/** Form per aggiungere un giocatore alla rosa (DD-017): l'id `g<N>` è calcolato in app,
|
||||
* il database non lo genera da solo. */
|
||||
function AggiungiGiocatore({ righe }: { righe: GiocatoreSquadra[] }) {
|
||||
const [aperto, setAperto] = useState(false);
|
||||
const [dati, setDati] = useState<DatiSquadra>(datiVuoti);
|
||||
const aggiungi = useAggiungiGiocatore();
|
||||
|
||||
async function conferma() {
|
||||
const errore = validaDatiSquadra(dati);
|
||||
if (errore) {
|
||||
toast.error(errore);
|
||||
return;
|
||||
}
|
||||
if (numeroGiaUsato(righe, "", dati.numero)) {
|
||||
toast.warning(`Il numero ${dati.numero} è già assegnato a un altro giocatore.`);
|
||||
}
|
||||
try {
|
||||
await aggiungi.mutateAsync({ id: prossimoIdGiocatore(righe), dati });
|
||||
toast.success(`${dati.nome} ${dati.cognome} aggiunto alla rosa`);
|
||||
setDati(datiVuoti);
|
||||
setAperto(false);
|
||||
} catch (e) {
|
||||
toast.error(messaggioErrore(e, "Aggiunta non riuscita"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!aperto) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAperto(true)}
|
||||
className="premi mt-3 flex w-full items-center justify-center gap-2 rounded-2xl bg-secondary py-3 text-sm font-bold uppercase"
|
||||
>
|
||||
<UserPlus className="h-4 w-4" /> Aggiungi giocatore
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-3 rounded-2xl bg-card p-4 shadow-card">
|
||||
<h3 className="font-display text-sm uppercase tracking-wide">Nuovo giocatore</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Campo label="Nome">
|
||||
<input
|
||||
value={dati.nome}
|
||||
maxLength={40}
|
||||
onChange={(e) => setDati({ ...dati, nome: e.target.value })}
|
||||
className={classiInput}
|
||||
/>
|
||||
</Campo>
|
||||
<Campo label="Cognome">
|
||||
<input
|
||||
value={dati.cognome}
|
||||
maxLength={40}
|
||||
onChange={(e) => setDati({ ...dati, cognome: e.target.value })}
|
||||
className={classiInput}
|
||||
/>
|
||||
</Campo>
|
||||
<Campo label="Numero">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={dati.numero}
|
||||
onChange={(e) => setDati({ ...dati, numero: Number(e.target.value) })}
|
||||
className={classiInput}
|
||||
/>
|
||||
</Campo>
|
||||
<Campo label="Ruolo">
|
||||
<input
|
||||
value={dati.ruolo}
|
||||
maxLength={30}
|
||||
onChange={(e) => setDati({ ...dati, ruolo: e.target.value })}
|
||||
className={classiInput}
|
||||
/>
|
||||
</Campo>
|
||||
</div>
|
||||
<Campo label="Email (collegamento automatico al login, opzionale)">
|
||||
<input
|
||||
type="email"
|
||||
value={dati.email ?? ""}
|
||||
placeholder="nome@esempio.com"
|
||||
onChange={(e) => setDati({ ...dati, email: e.target.value || null })}
|
||||
className={classiInput}
|
||||
/>
|
||||
</Campo>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAperto(false);
|
||||
setDati(datiVuoti);
|
||||
}}
|
||||
className="premi flex-1 rounded-2xl bg-secondary py-2.5 text-xs font-bold uppercase"
|
||||
>
|
||||
Annulla
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void conferma()}
|
||||
disabled={aggiungi.isPending}
|
||||
className="premi flex-1 rounded-2xl bg-primary py-2.5 text-xs font-bold uppercase text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
Aggiungi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Riga compatta per riattivare un giocatore che aveva lasciato la squadra. */
|
||||
function GiocatoreDisattivato({ g }: { g: GiocatoreSquadra }) {
|
||||
const impostaAttivo = useImpostaAttivo();
|
||||
|
||||
async function riattiva() {
|
||||
if (!confirm(`Riattivare ${nomeCompleto(g)}?`)) return;
|
||||
try {
|
||||
await impostaAttivo.mutateAsync({ giocatoreId: g.id, attivo: true });
|
||||
toast.success("Giocatore riattivato");
|
||||
} catch (e) {
|
||||
toast.error(messaggioErrore(e, "Operazione non riuscita"));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3 rounded-2xl bg-card p-3 shadow-card">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-semibold leading-tight">{nomeCompleto(g)}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
#{g.numero} · {g.ruolo}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void riattiva()}
|
||||
disabled={impostaAttivo.isPending}
|
||||
className="premi flex shrink-0 items-center gap-1.5 rounded-xl bg-success px-3 py-2 text-xs font-bold text-success-foreground disabled:opacity-50"
|
||||
>
|
||||
<UserCheck className="h-3.5 w-3.5" /> Riattiva
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dashboard() {
|
||||
const admin = useIsAdmin();
|
||||
const { righe: squadra } = useGiocatoriSquadra();
|
||||
@@ -369,6 +574,7 @@ function Dashboard() {
|
||||
}
|
||||
|
||||
const attivi = squadra.filter((g) => g.attivo);
|
||||
const disattivi = squadra.filter((g) => !g.attivo);
|
||||
const completi = attivi.filter((g) => completamento(profili[g.id]) === 100).length;
|
||||
const certificatiOk = attivi.filter(
|
||||
(g) =>
|
||||
@@ -399,6 +605,7 @@ function Dashboard() {
|
||||
>
|
||||
<Download className="h-4 w-4" /> Esporta CSV tesseramento
|
||||
</button>
|
||||
<AggiungiGiocatore righe={squadra} />
|
||||
</Section>
|
||||
|
||||
<Section titolo="Profili" indice={1}>
|
||||
@@ -414,6 +621,16 @@ function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{disattivi.length > 0 ? (
|
||||
<Section titolo="Giocatori disattivati" indice={2}>
|
||||
<div className="space-y-2">
|
||||
{disattivi.map((g) => (
|
||||
<GiocatoreDisattivato key={g.id} g={g} />
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
dividiNome,
|
||||
nomeCompleto,
|
||||
numeroGiaUsato,
|
||||
prossimoIdGiocatore,
|
||||
rosaFallback,
|
||||
slotDi,
|
||||
slotPerEmail,
|
||||
@@ -84,7 +85,7 @@ assert.equal(
|
||||
assert.equal(slotPerEmail(rosaEmail, ""), null, "stringa vuota trattata come nessuna email");
|
||||
|
||||
// --- validaDatiSquadra ----------------------------------------------------------------
|
||||
const datiOk = { nome: "Mario", cognome: "Rossi", numero: 7, ruolo: "schiacciatore" };
|
||||
const datiOk = { nome: "Mario", cognome: "Rossi", numero: 7, ruolo: "schiacciatore", email: null };
|
||||
assert.equal(validaDatiSquadra(datiOk), null, "dati validi: nessun errore");
|
||||
assert.match(validaDatiSquadra({ ...datiOk, nome: "" })!, /nome/i);
|
||||
assert.match(
|
||||
@@ -105,6 +106,29 @@ assert.match(
|
||||
/numero/i,
|
||||
"il numero deve essere intero",
|
||||
);
|
||||
assert.equal(
|
||||
validaDatiSquadra({ ...datiOk, email: "foo@bar.com" }),
|
||||
null,
|
||||
"email valida: nessun errore",
|
||||
);
|
||||
assert.equal(
|
||||
validaDatiSquadra({ ...datiOk, email: "" }),
|
||||
null,
|
||||
"email vuota è ammessa: non ancora nota",
|
||||
);
|
||||
assert.match(
|
||||
validaDatiSquadra({ ...datiOk, email: "non-e-una-email" })!,
|
||||
/email/i,
|
||||
"senza @ l'email non è valida",
|
||||
);
|
||||
|
||||
// --- prossimoIdGiocatore ----------------------------------------------------------------
|
||||
assert.equal(
|
||||
prossimoIdGiocatore([riga({ id: "g1" }), riga({ id: "g3" }), riga({ id: "g2" })]),
|
||||
"g4",
|
||||
"il prossimo id segue il numero più alto già in uso",
|
||||
);
|
||||
assert.equal(prossimoIdGiocatore([]), "g1", "rosa vuota: si parte da g1");
|
||||
|
||||
// --- numeroGiaUsato ----------------------------------------------------------------
|
||||
const rosaNumeri: GiocatoreSquadra[] = [
|
||||
|
||||
@@ -149,7 +149,7 @@ assert.equal(slotDi(squadra, "u2")?.id, "g2");
|
||||
assert.equal(slotDi(squadra, "sconosciuto"), null);
|
||||
|
||||
// --- dati squadra modificabili dall'admin (DD-017) ---------------------------
|
||||
const datiOk = { nome: "Ivan", cognome: "Cacciari", numero: 23, ruolo: "Banda" };
|
||||
const datiOk = { nome: "Ivan", cognome: "Cacciari", numero: 23, ruolo: "Banda", email: null };
|
||||
assert.equal(validaDatiSquadra(datiOk), null);
|
||||
assert.match(validaDatiSquadra({ ...datiOk, nome: " " }) ?? "", /nome/i);
|
||||
assert.match(validaDatiSquadra({ ...datiOk, cognome: "" }) ?? "", /cognome/i);
|
||||
|
||||
Reference in New Issue
Block a user