Files
CRAPP/src/components/crapp/TurnoPalloni.tsx
T
davideandClaude Sonnet 5 3258a24f20 Estende il fix di useIo/useRosa a EventoCard e altri 13 usi non statistici
EventoCard monta un hook per ogni card mostrata: usava useGiocatoreCorrente
(= useIo = useRosa, le 6 statistiche pesanti) solo per leggere il proprio id.
Il Calendario ne rende diverse insieme (prossimi eventi, compleanni, drawer del
giorno), quindi ogni apertura moltiplicava il ricalcolo dell'intera rosa.

Audit di tutti gli altri usi di useGiocatoreCorrente: 12 su 13 leggevano solo
id/nome/verita', mai una statistica. Corretti allo stesso modo (-> useGiocatoreBase):
benvenuto.tsx, VotazioneMvp, SondaggioCacche, VotoSocial, eventi.tsx,
PromemoriaPalloni (Home), TurnoPalloni, ScoutEntry, Pagelle.

partita.$id.tsx: `io` era dichiarato e mai piu' usato, rimosso.

Stesso pattern trovato anche su useRosa (non solo useGiocatoreCorrente) in
scout.tsx e RosaPresenze.tsx (montata su partita e allenamento): entrambi
passati al nuovo useAnagraficaRosa, esteso con ruolo e numero.

Unica eccezione: CelebrazioneBadge.tsx usa davvero le statistiche complete
(badge, MVP, serie) ed e' montato globalmente in __root.tsx - non downgradabile,
resta il costo di base piu' alto rimasto in giro.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 10:00:52 +02:00

133 lines
5.2 KiB
TypeScript

import { useState } from "react";
import { BellRing, Check, CircleDot, Loader2, Pencil } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { Avatar } from "@/components/crapp/Avatar";
import { intestazioniAutenticate } from "@/lib/auth";
import { nomeCompleto, useGiocatoriSquadra } from "@/lib/giocatori-squadra";
import { useAssegnaTurno, useTurniPalloni } from "@/lib/palloni";
import { useIsAdmin } from "@/lib/ruoli";
import { useGiocatoreBase } from "@/lib/user-store";
export function TurnoPalloni({ eventoId }: { eventoId: string }) {
const [aperto, setAperto] = useState(false);
const [avviso, setAvviso] = useState(false);
const { salvati, turni, isPending } = useTurniPalloni();
const assegna = useAssegnaTurno();
// Solo `.nome` serve qui: `useGiocatoreBase` basta, niente statistiche.
const io = useGiocatoreBase();
const admin = useIsAdmin();
const { righe: squadra } = useGiocatoriSquadra();
const rosa = squadra.filter((g) => g.attivo);
const id = turni[eventoId];
const proposto = !salvati[eventoId];
const giocatore = rosa.find((g) => g.id === id);
function scegli(giocatoreId: string) {
setAperto(false);
assegna.mutate(
{ eventoId, giocatoreId, da: io?.nome ?? null },
{
onSuccess: () => toast.success("Turno palloni aggiornato"),
onError: () => toast.error("Non sono riuscito a salvare il turno"),
},
);
}
/** Avvisa via push chi è di turno per questo evento, e chi deve riportare i palloni. */
async function avvisa() {
setAvviso(true);
try {
const res = await fetch("/api/public/promemoria-palloni", {
method: "POST",
headers: { "Content-Type": "application/json", ...(await intestazioniAutenticate()) },
body: JSON.stringify({ eventoId }),
});
if (!res.ok) throw new Error();
const dati = (await res.json()) as { inviate: number; destinatari: number };
if (dati.inviate > 0) {
toast.success(`Promemoria inviato a ${dati.inviate} dispositivi`);
} else if (dati.destinatari === 0) {
toast.info("Nessun dispositivo con le notifiche attive tra gli incaricati");
} else {
// I dispositivi ci sono ma l'invio non è riuscito: quasi sempre le chiavi VAPID
// mancanti (in locale non sono configurate). Dirlo, invece di dare la colpa
// all'assenza di iscrizioni.
toast.error(`Invio non riuscito verso ${dati.destinatari} dispositivi`);
}
} catch {
toast.error("Non sono riuscito a inviare il promemoria");
} finally {
setAvviso(false);
}
}
return (
<div className="mt-3 rounded-2xl bg-secondary/60 p-2.5">
<button
type="button"
onClick={() => setAperto((v) => !v)}
className="flex w-full items-center gap-2.5 text-left"
>
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-xl bg-card text-accent">
<CircleDot className="h-4 w-4" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-xs font-bold uppercase tracking-wide text-muted-foreground">
Palloni
</span>
<span className="flex items-center gap-1.5 text-sm font-bold leading-tight">
{isPending || assegna.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin text-muted-foreground" />
) : null}
<span className="truncate">{giocatore ? nomeCompleto(giocatore) : "Da assegnare"}</span>
{proposto && giocatore ? (
<span className="shrink-0 rounded-full bg-card px-1.5 py-0.5 text-xs font-bold uppercase text-muted-foreground">
proposto
</span>
) : null}
</span>
</span>
<Pencil className="h-4 w-4 shrink-0 text-muted-foreground" />
</button>
{aperto ? (
<div className="mt-2.5 max-h-56 space-y-1 overflow-y-auto rounded-xl bg-card p-1.5">
{rosa.map((g) => (
<button
key={g.id}
type="button"
onClick={() => scegli(g.id)}
className={cn(
"flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors",
g.id === id ? "bg-accent/10 font-bold" : "hover:bg-secondary",
)}
>
<Avatar id={g.id} fallback={String(g.numero)} className="h-7 w-7 text-xs" />
<span className="min-w-0 flex-1 truncate">{nomeCompleto(g)}</span>
{g.id === id ? <Check className="h-4 w-4 shrink-0 text-accent" /> : null}
</button>
))}
</div>
) : null}
{admin && giocatore ? (
<button
type="button"
onClick={avvisa}
disabled={avviso}
className="premi mt-2.5 flex w-full items-center justify-center gap-2 rounded-xl bg-card px-3 py-2 text-xs font-bold uppercase text-foreground disabled:opacity-50"
>
{avviso ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<BellRing className="h-3.5 w-3.5" />
)}
Avvisa chi è di turno
</button>
) : null}
</div>
);
}