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>
271 lines
10 KiB
TypeScript
271 lines
10 KiB
TypeScript
import { createFileRoute } from "@tanstack/react-router";
|
|
import { MapPin, Clock, Users, Trophy, Swords, Download } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Card, PageHeader, Section } from "@/components/crapp/ui-bits";
|
|
import { formatData } from "@/lib/crapp-data";
|
|
import { convocatiEvento, useEvento } from "@/lib/eventi";
|
|
import { useRosa } from "@/lib/rosa";
|
|
import { useCsi } from "@/lib/csi";
|
|
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
|
import { Pagelle } from "@/components/crapp/Pagelle";
|
|
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
|
|
import { ScoutEntry } from "@/components/crapp/ScoutEntry";
|
|
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
|
|
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
|
|
import { useIsAdmin } from "@/lib/ruoli";
|
|
import { VotazioneMvp } from "@/components/crapp/VotazioneMvp";
|
|
import { VotoSocial } from "@/components/crapp/VotoSocial";
|
|
import { TurnoPalloni } from "@/components/crapp/TurnoPalloni";
|
|
import { RosaPresenze } from "@/components/crapp/RosaPresenze";
|
|
import { usePresenzeEvento } from "@/lib/presenze";
|
|
|
|
export const Route = createFileRoute("/partita/$id")({
|
|
head: () => {
|
|
const titolo = "Dettaglio partita";
|
|
return {
|
|
meta: [
|
|
{ title: `${titolo} — CrAPP` },
|
|
{
|
|
name: "description",
|
|
content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
|
|
},
|
|
{ property: "og:title", content: `${titolo} — CrAPP` },
|
|
{
|
|
property: "og:description",
|
|
content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
|
|
},
|
|
{ property: "og:type", content: "website" },
|
|
{ name: "twitter:card", content: "summary" },
|
|
],
|
|
};
|
|
},
|
|
component: PartitaDetail,
|
|
});
|
|
|
|
function PartitaDetail() {
|
|
const { id } = Route.useParams();
|
|
const { evento } = useEvento(id);
|
|
const admin = useIsAdmin();
|
|
const scoutMatches = useScoutMatches();
|
|
const { data: csi } = useCsi();
|
|
const { risposte } = usePresenzeEvento(id);
|
|
const rosa = useRosa();
|
|
const presentiVeri = rosa.filter(
|
|
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
|
|
).length;
|
|
|
|
if (!evento) {
|
|
return (
|
|
<div className="px-5 pt-8">
|
|
<p className="mt-8 text-center text-sm text-muted-foreground">Partita non trovata</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const convocati = convocatiEvento(evento, rosa);
|
|
const scout = scoutMatches.find((m) => m.id === evento.id || m.data === evento.data) ?? null;
|
|
const csiMatch = csi
|
|
? partiteGiocate(csi.partite).find((p) => p.data === evento.data)
|
|
: undefined;
|
|
const match = scout
|
|
? {
|
|
id: scout.id,
|
|
data: scout.data,
|
|
avversario: scout.avversario,
|
|
casa: scout.casa,
|
|
setNostri: scout.setNostri,
|
|
setLoro: scout.setLoro,
|
|
parziali: scout.parziali,
|
|
}
|
|
: csiMatch
|
|
? matchDaPartitaCsi(csiMatch)
|
|
: undefined;
|
|
const totaliTeam = scout ? totaliSquadra([scout]) : null;
|
|
const avversario = evento.titolo.includes(" vs ")
|
|
? (evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo)
|
|
: evento.titolo;
|
|
const casa = evento.casa;
|
|
const vinta = match && match.setNostri > match.setLoro;
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
titolo={evento.campionato ? "Partita" : "Amichevole"}
|
|
sottotitolo={formatData(evento.data)}
|
|
/>
|
|
|
|
<Section titolo={evento.titolo}>
|
|
<div className="rounded-3xl bg-card p-5 shadow-card">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={cn(
|
|
"grid h-12 w-12 shrink-0 place-items-center rounded-2xl",
|
|
casa ? "bg-primary/15 text-primary" : "bg-accent/15 text-accent",
|
|
)}
|
|
>
|
|
{casa ? <Swords className="h-6 w-6" /> : <Trophy className="h-6 w-6" />}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
|
{casa ? "In casa" : "Fuori casa"}
|
|
{evento.campionato ? " · Campionato" : " · Amichevole"}
|
|
</p>
|
|
<p className="truncate text-lg font-bold leading-tight">{avversario}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 space-y-2 text-sm text-muted-foreground">
|
|
<span className="inline-flex items-center gap-2">
|
|
<Clock className="h-4 w-4" /> {evento.ora}
|
|
</span>
|
|
<span className="ml-4 inline-flex items-center gap-2">
|
|
<MapPin className="h-4 w-4" /> {evento.luogo}
|
|
</span>
|
|
</div>
|
|
|
|
{evento.note ? (
|
|
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-sm">
|
|
{evento.note}
|
|
</p>
|
|
) : null}
|
|
|
|
<div className="mt-4 inline-flex items-center gap-2 rounded-full bg-secondary px-3 py-1.5 text-xs font-semibold">
|
|
<Users className="h-4 w-4" />
|
|
Conferme: {presentiVeri}/{convocati.length}
|
|
</div>
|
|
|
|
<TurnoPalloni eventoId={evento.id} />
|
|
</div>
|
|
</Section>
|
|
|
|
{match ? (
|
|
<Section titolo="Risultato">
|
|
<div className="rounded-3xl bg-card p-5 shadow-card">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-bold">CRAP Volley</span>
|
|
<span
|
|
className={cn(
|
|
"rounded-full px-3 py-1 text-lg font-display font-bold",
|
|
vinta
|
|
? "bg-success text-success-foreground"
|
|
: "bg-destructive text-destructive-foreground",
|
|
)}
|
|
>
|
|
{match.setNostri} - {match.setLoro}
|
|
</span>
|
|
<span className="text-right text-sm font-bold">{avversario}</span>
|
|
</div>
|
|
<div className="mt-4 space-y-2">
|
|
{match.parziali.map(([noi, loro], i) => (
|
|
<div
|
|
key={i}
|
|
className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2"
|
|
>
|
|
<span className="font-display text-lg">{noi}</span>
|
|
<span className="text-xs font-semibold text-muted-foreground">Set {i + 1}</span>
|
|
<span className="font-display text-lg">{loro}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
) : null}
|
|
|
|
<Section titolo="MVP eletto dalla squadra">
|
|
<div className="rounded-3xl bg-card p-5 shadow-card">
|
|
<VotazioneMvp evento={evento} />
|
|
</div>
|
|
</Section>
|
|
|
|
{match ? (
|
|
<Section titolo="Badge votati dai compagni">
|
|
<VotoSocial matchId={match.id} />
|
|
</Section>
|
|
) : (
|
|
<Section titolo="In programma">
|
|
<p className="rounded-3xl bg-card p-5 text-center text-sm text-muted-foreground shadow-card">
|
|
La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il
|
|
risultato.
|
|
</p>
|
|
</Section>
|
|
)}
|
|
|
|
<Section titolo="Scout live">
|
|
<ScoutEntry eventoId={evento.id} />
|
|
</Section>
|
|
|
|
<Section titolo="Sondaggio pre-partita">
|
|
<SondaggioCacche eventoId={evento.id} dataEvento={evento.data} oraEvento={evento.ora} />
|
|
</Section>
|
|
|
|
{match ? (
|
|
<Section titolo="Pagelle di fine partita">
|
|
<Pagelle matchId={match.id} convocati={convocati} chiuse={evento.pagelleChiuse} />
|
|
</Section>
|
|
) : null}
|
|
|
|
{scout && totaliTeam ? (
|
|
<Section titolo="Report tecnico">
|
|
<Card>
|
|
<div className="grid grid-cols-4 gap-2 text-center">
|
|
{[
|
|
{ l: "Punti", v: totaliTeam.punti },
|
|
{ l: "Ace", v: totaliTeam.ace },
|
|
{ l: "Muri", v: totaliTeam.muri },
|
|
{ l: "Errori", v: totaliTeam.errori },
|
|
].map((t) => (
|
|
<div key={t.l} className="rounded-2xl bg-secondary p-2.5">
|
|
<p className="font-display text-xl leading-none">{t.v}</p>
|
|
<p className="mt-1 text-xs font-semibold uppercase text-muted-foreground">
|
|
{t.l}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="mt-4 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
|
Dettaglio giocatori (uso interno allenatori)
|
|
</p>
|
|
<div className="mt-2 space-y-1">
|
|
{[...totaliPerGiocatore(scout.azioni).entries()].map(([gid, t]) => {
|
|
const g = rosa.find((x) => x.id === gid);
|
|
if (!g) return null;
|
|
return (
|
|
<div
|
|
key={gid}
|
|
className="flex items-center gap-2 rounded-xl bg-secondary/60 px-3 py-2 text-xs"
|
|
>
|
|
<span className="min-w-0 flex-1 truncate font-semibold">
|
|
#{g.numero} {g.nome}
|
|
</span>
|
|
<span className="tabular-nums text-muted-foreground">
|
|
{t.punti}P · {t.ace}A · {t.muri}M · {t.errori}E
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{admin ? (
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
scaricaCsv(
|
|
`scout-${scout.data}-${scout.avversario}.csv`,
|
|
csvScoutMatch(scout, rosa),
|
|
)
|
|
}
|
|
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-accent-grad py-3 text-sm font-bold uppercase text-accent-foreground shadow-pop"
|
|
>
|
|
<Download className="h-4 w-4" /> Esporta CSV
|
|
</button>
|
|
) : null}
|
|
</Card>
|
|
</Section>
|
|
) : null}
|
|
|
|
<Section titolo="Rosa e presenze">
|
|
<RosaPresenze eventoId={evento.id} data={evento.data} />
|
|
</Section>
|
|
</>
|
|
);
|
|
}
|