import { useEventiVisibili } from "@/lib/eventi-visibili"; import { useEffect, useState } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { CalendarPlus, ChevronLeft, ChevronRight, Loader2, Pencil, Trash2, X } from "lucide-react"; import { toast } from "sonner"; import { AnimatePresence, motion } from "motion/react"; import { cn } from "@/lib/utils"; import { Campo, Card, classiInput, PageHeader, Section } from "@/components/crapp/ui-bits"; import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, } from "@/components/ui/drawer"; import { formatData } from "@/lib/crapp-data"; import { giorniDelMese, giorniIT, mesiIT, pad2, useMeseNav } from "@/lib/calendario"; import { inRosa, nomeCompleto, useGiocatoriSquadra } from "@/lib/giocatori-squadra"; import { categoriaEvento, daCategoria, eventoVuoto, linkMapsValido, useEliminaEvento, useSalvaEvento, type CategoriaEvento, type Evento, } from "@/lib/eventi"; import { useGiocatoreBase } from "@/lib/user-store"; import { usePuoGestireEventi } from "@/lib/ruoli"; import { molla, proietta } from "@/lib/molla"; import { useMotoRidotto } from "@/lib/motion"; export const Route = createFileRoute("/eventi")({ head: () => ({ meta: [ { title: "Gestione eventi — CrAPP" }, { name: "description", content: "Area riservata ai referenti CRAP Volley: crea, modifica ed elimina allenamenti, partite ed eventi di squadra.", }, { property: "og:title", content: "Gestione eventi — CrAPP" }, { property: "og:description", content: "Crea e modifica il calendario della squadra e scegli i convocati.", }, { property: "og:type", content: "website" }, { name: "twitter:card", content: "summary" }, ], }), component: GestioneEventi, }); const tipi: Array<{ id: CategoriaEvento; label: string }> = [ { id: "allenamento", label: "Allenamento" }, { id: "partita", label: "Partita" }, { id: "amichevole", label: "Amichevole" }, { id: "evento", label: "Evento" }, ]; function GestioneEventi() { // Solo verità (`!io`, gate admin o allenatore): `useGiocatoreBase` basta, niente statistiche. const io = useGiocatoreBase(); // Admin o allenatore (DD-034): la RLS di `eventi_app` (M21) dice lo stesso. const puoGestire = usePuoGestireEventi(); const { eventi, isPending, isError, error, refetch } = useEventiVisibili(); const { righe: squadra } = useGiocatoriSquadra(); const salva = useSalvaEvento(); const elimina = useEliminaEvento(); const [bozza, setBozza] = useState(null); const [daEliminare, setDaEliminare] = useState(null); const [confermaModifica, setConfermaModifica] = useState(false); const [cambiaGiorno, setCambiaGiorno] = useState(false); const rosa = squadra.filter(inRosa); // SSR-safe: la data di oggi arriva solo dopo il mount. const [oggi, setOggi] = useState<{ anno: number; mese: number; giorno: number } | null>(null); useEffect(() => { const d = new Date(); setOggi({ anno: d.getFullYear(), mese: d.getMonth(), giorno: d.getDate() }); }, []); const [giornoSelezionato, setGiornoSelezionato] = useState(null); const ridotto = useMotoRidotto(); const { anno, mese, direzione, precedente, successivo } = useMeseNav(); const { giorni, offsetLunedi } = giorniDelMese(anno, mese); if (!io || !puoGestire) { return ( <>

Solo i referenti della squadra possono creare o modificare gli eventi.

); } function aggiorna(patch: Partial) { setBozza((b) => (b ? { ...b, ...patch } : b)); } const modificaEsistente = bozza ? eventi.some((e) => e.id === bozza.id) : false; function chiediConferma() { if (!bozza) return; if (!bozza.titolo.trim()) { toast.error("Serve un titolo per l'evento"); return; } if ((bozza.linkMaps ?? "").trim() && !linkMapsValido(bozza.linkMaps!.trim())) { toast.error( "Il link deve essere di Google Maps (https://maps.app.goo.gl/… o google.com/maps…)", ); return; } if (modificaEsistente) { setConfermaModifica(true); return; } conferma(); } async function conferma() { if (!bozza) return; try { await salva.mutateAsync({ ...bozza, titolo: bozza.titolo.trim(), linkMaps: (bozza.linkMaps ?? "").trim(), }); toast.success("Evento salvato"); setBozza(null); } catch { toast.error("Non sono riuscito a salvare l'evento"); } finally { setConfermaModifica(false); } } async function rimuovi(id: string) { try { await elimina.mutateAsync(id); toast.success("Evento eliminato"); if (bozza?.id === id) setBozza(null); } catch { toast.error("Non sono riuscito a eliminare l'evento"); } finally { setDaEliminare(null); } } const mesePrefix = `${anno}-${pad2(mese + 1)}`; const eventiPerGiorno = new Map(); for (const e of eventi) { if (!e.data.startsWith(mesePrefix)) continue; const g = Number(e.data.slice(8, 10)); const lista = eventiPerGiorno.get(g) ?? []; lista.push(e); eventiPerGiorno.set(g, lista); } function apriGiorno(giorno: number) { setGiornoSelezionato(`${mesePrefix}-${pad2(giorno)}`); } function nuovoNelGiorno() { if (!giornoSelezionato) return; setBozza({ ...eventoVuoto(), data: giornoSelezionato }); setCambiaGiorno(false); setGiornoSelezionato(null); } function modificaDalGiorno(e: Evento) { setBozza(e); setCambiaGiorno(false); setGiornoSelezionato(null); } function eliminaDalGiorno(e: Evento) { setDaEliminare(e); setGiornoSelezionato(null); } const eventiGiornoSelezionato = giornoSelezionato ? eventi.filter((e) => e.data === giornoSelezionato) : []; return ( <>
{mesiIT[mese]} {anno}
{giorniIT.map((g, i) => ( {g} ))}
{/* Swipe tra mesi con la stessa fisica di `/calendario` (vedi il commento lì). */}
{ const arrivo = info.offset.x + proietta(info.velocity.x); if (arrivo < -60) successivo(); else if (arrivo > 60) precedente(); }} initial={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * 48 }} animate={{ opacity: 1, x: 0 }} exit={ridotto ? { opacity: 0 } : { opacity: 0, x: direzione * -48 }} transition={ridotto ? { duration: 0.2 } : molla.foglio} className="grid touch-pan-y grid-cols-7 gap-1" > {Array.from({ length: offsetLunedi }).map((_, i) => ( ))} {Array.from({ length: giorni }).map((_, i) => { const giorno = i + 1; const eventiGiorno = eventiPerGiorno.get(giorno) ?? []; const haEventi = eventiGiorno.length > 0; const isOggi = !!oggi && oggi.anno === anno && oggi.mese === mese && oggi.giorno === giorno; return ( ); })}
{isPending ? (

Carico gli eventi…

) : isError ? (

Non sono riuscito a caricare gli eventi {error instanceof Error ? `: ${error.message}` : ""}.

) : (

Scorri a destra o sinistra per cambiare mese. Tocca un giorno per aggiungere, modificare o eliminare.

)}
{bozza ? (
e.id === bozza.id) ? "Modifica evento" : "Nuovo evento"} >
{tipi.map((t) => ( ))}
aggiorna({ titolo: e.target.value })} className={classiInput} />
{/* Il giorno arriva già dal calendario: si mostra e basta, il campo data compare solo per spostare l'evento. */} {cambiaGiorno ? ( aggiorna({ data: e.target.value })} className={classiInput} /> ) : (
Giorno
{formatData(bozza.data)}
)} aggiorna({ ora: e.target.value })} className={classiInput} />
aggiorna({ luogo: e.target.value })} className={classiInput} /> aggiorna({ linkMaps: e.target.value })} className={classiInput} />