2026-08-06 08:36:47 +02:00
|
|
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
|
|
|
|
import { ArrowLeft, MapPin, Clock, Users, Trophy, Swords, Download } from "lucide-react";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2026-09-05 12:42:45 +02:00
|
|
|
import { Card, PageHeader, Section } from "@/components/crapp/ui-bits";
|
2026-09-03 10:17:03 +02:00
|
|
|
import { formatData } from "@/lib/crapp-data";
|
2026-08-06 08:36:47 +02:00
|
|
|
import { convocatiEvento, useEvento } from "@/lib/eventi";
|
2026-09-03 10:17:03 +02:00
|
|
|
import { useRosa } from "@/lib/rosa";
|
2026-09-01 13:38:33 +02:00
|
|
|
import { useCsi } from "@/lib/csi";
|
|
|
|
|
import { matchDaPartitaCsi, partiteGiocate } from "@/lib/csi-core";
|
2026-08-06 08:36:47 +02:00
|
|
|
import { Pagelle } from "@/components/crapp/Pagelle";
|
|
|
|
|
import { SondaggioCacche } from "@/components/crapp/SondaggioCacche";
|
|
|
|
|
import { useScoutMatches, totaliPerGiocatore, totaliSquadra } from "@/lib/scout-store";
|
|
|
|
|
import { csvScoutMatch, scaricaCsv } from "@/lib/scout-export";
|
|
|
|
|
import { useGiocatoreCorrente } from "@/lib/user-store";
|
2026-08-30 17:57:59 +02:00
|
|
|
import { useIsAdmin } from "@/lib/ruoli";
|
2026-08-06 08:36:47 +02:00
|
|
|
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` },
|
2026-09-01 13:38:33 +02:00
|
|
|
{
|
|
|
|
|
name: "description",
|
|
|
|
|
content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
|
|
|
|
|
},
|
2026-08-06 08:36:47 +02:00
|
|
|
{ property: "og:title", content: `${titolo} — CrAPP` },
|
2026-09-01 13:38:33 +02:00
|
|
|
{
|
|
|
|
|
property: "og:description",
|
|
|
|
|
content: "Dettaglio partita, formazione e risultati del CRAP Volley.",
|
|
|
|
|
},
|
2026-08-06 08:36:47 +02:00
|
|
|
{ property: "og:type", content: "website" },
|
|
|
|
|
{ name: "twitter:card", content: "summary" },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
component: PartitaDetail,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function PartitaDetail() {
|
|
|
|
|
const { id } = Route.useParams();
|
|
|
|
|
const { evento } = useEvento(id);
|
|
|
|
|
const io = useGiocatoreCorrente();
|
2026-08-30 17:57:59 +02:00
|
|
|
const admin = useIsAdmin();
|
2026-08-06 08:36:47 +02:00
|
|
|
const scoutMatches = useScoutMatches();
|
2026-09-01 13:38:33 +02:00
|
|
|
const { data: csi } = useCsi();
|
2026-08-06 08:36:47 +02:00
|
|
|
const { risposte } = usePresenzeEvento(id);
|
2026-09-03 10:17:03 +02:00
|
|
|
const rosa = useRosa();
|
|
|
|
|
const presentiVeri = rosa.filter(
|
2026-08-06 08:36:47 +02:00
|
|
|
(g) => risposte[g.id] === "presente" || risposte[g.id] === "ritardo",
|
|
|
|
|
).length;
|
|
|
|
|
|
|
|
|
|
if (!evento) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="px-5 pt-8">
|
|
|
|
|
<Link
|
|
|
|
|
to="/calendario"
|
|
|
|
|
className="inline-flex items-center gap-1 text-sm font-semibold text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeft className="h-4 w-4" /> Torna al calendario
|
|
|
|
|
</Link>
|
|
|
|
|
<p className="mt-8 text-center text-sm text-muted-foreground">Partita non trovata</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 10:17:03 +02:00
|
|
|
const convocati = convocatiEvento(evento, rosa);
|
2026-08-06 08:36:47 +02:00
|
|
|
const scout = scoutMatches.find((m) => m.id === evento.id || m.data === evento.data) ?? null;
|
2026-09-01 13:38:33 +02:00
|
|
|
const csiMatch = csi
|
|
|
|
|
? partiteGiocate(csi.partite).find((p) => p.data === evento.data)
|
|
|
|
|
: undefined;
|
2026-08-06 08:36:47 +02:00
|
|
|
const match = scout
|
|
|
|
|
? {
|
|
|
|
|
id: scout.id,
|
|
|
|
|
data: scout.data,
|
|
|
|
|
avversario: scout.avversario,
|
|
|
|
|
casa: scout.casa,
|
|
|
|
|
setNostri: scout.setNostri,
|
|
|
|
|
setLoro: scout.setLoro,
|
|
|
|
|
parziali: scout.parziali,
|
|
|
|
|
}
|
2026-09-01 13:38:33 +02:00
|
|
|
: csiMatch
|
|
|
|
|
? matchDaPartitaCsi(csiMatch)
|
|
|
|
|
: undefined;
|
2026-08-06 08:36:47 +02:00
|
|
|
const totaliTeam = scout ? totaliSquadra([scout]) : null;
|
|
|
|
|
const avversario = evento.titolo.includes(" vs ")
|
2026-09-01 13:38:33 +02:00
|
|
|
? (evento.titolo.split(" vs ").find((p) => !p.includes("CRAP")) ?? evento.titolo)
|
2026-08-06 08:36:47 +02:00
|
|
|
: evento.titolo;
|
|
|
|
|
const casa = evento.casa;
|
|
|
|
|
const vinta = match && match.setNostri > match.setLoro;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="px-5 pt-4">
|
|
|
|
|
<Link
|
|
|
|
|
to="/calendario"
|
|
|
|
|
className="inline-flex items-center gap-1 text-sm font-semibold text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeft className="h-4 w-4" /> Calendario
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
2026-09-05 23:50:51 +02:00
|
|
|
{evento.note ? (
|
|
|
|
|
<p className="mt-3 whitespace-pre-line rounded-2xl bg-secondary px-3 py-2 text-sm">
|
|
|
|
|
{evento.note}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
|
2026-08-06 08:36:47 +02:00
|
|
|
<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",
|
2026-09-01 13:38:33 +02:00
|
|
|
vinta
|
|
|
|
|
? "bg-success text-success-foreground"
|
|
|
|
|
: "bg-destructive text-destructive-foreground",
|
2026-08-06 08:36:47 +02:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{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) => (
|
2026-09-01 13:38:33 +02:00
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
className="flex items-center justify-between rounded-xl bg-secondary px-3 py-2"
|
|
|
|
|
>
|
2026-08-06 08:36:47 +02:00
|
|
|
<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>
|
|
|
|
|
<p className="mt-3 text-center text-xs font-semibold text-muted-foreground">
|
|
|
|
|
MVP eletto dalla squadra
|
|
|
|
|
</p>
|
|
|
|
|
<VotazioneMvp matchId={match.id} />
|
|
|
|
|
</div>
|
|
|
|
|
</Section>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{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">
|
2026-09-01 13:38:33 +02:00
|
|
|
La partita non è ancora stata disputata. Torna qui dopo il fischio finale per vedere il
|
|
|
|
|
risultato.
|
2026-08-06 08:36:47 +02:00
|
|
|
</p>
|
|
|
|
|
</Section>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Section titolo="Sondaggio pre-partita">
|
|
|
|
|
<SondaggioCacche eventoId={evento.id} />
|
|
|
|
|
</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">
|
2026-09-05 12:42:45 +02:00
|
|
|
<Card>
|
2026-08-06 08:36:47 +02:00
|
|
|
<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>
|
2026-09-05 12:42:45 +02:00
|
|
|
<p className="mt-1 text-xs font-semibold uppercase text-muted-foreground">
|
2026-08-06 08:36:47 +02:00
|
|
|
{t.l}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-09-05 12:42:45 +02:00
|
|
|
<p className="mt-4 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
2026-08-06 08:36:47 +02:00
|
|
|
Dettaglio giocatori (uso interno allenatori)
|
|
|
|
|
</p>
|
|
|
|
|
<div className="mt-2 space-y-1">
|
|
|
|
|
{[...totaliPerGiocatore(scout.azioni).entries()].map(([gid, t]) => {
|
2026-09-03 10:17:03 +02:00
|
|
|
const g = rosa.find((x) => x.id === gid);
|
2026-08-06 08:36:47 +02:00
|
|
|
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>
|
2026-08-30 17:57:59 +02:00
|
|
|
{admin ? (
|
2026-08-06 08:36:47 +02:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-09-01 13:38:33 +02:00
|
|
|
onClick={() =>
|
2026-09-03 10:17:03 +02:00
|
|
|
scaricaCsv(
|
|
|
|
|
`scout-${scout.data}-${scout.avversario}.csv`,
|
|
|
|
|
csvScoutMatch(scout, rosa),
|
|
|
|
|
)
|
2026-09-01 13:38:33 +02:00
|
|
|
}
|
2026-08-06 08:36:47 +02:00
|
|
|
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}
|
2026-09-05 12:42:45 +02:00
|
|
|
</Card>
|
2026-08-06 08:36:47 +02:00
|
|
|
</Section>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<Section titolo="Rosa e presenze">
|
|
|
|
|
<RosaPresenze eventoId={evento.id} />
|
|
|
|
|
</Section>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|