188 lines
6.4 KiB
TypeScript
188 lines
6.4 KiB
TypeScript
import { useState } from "react";
|
|
import { BellRing, HelpCircle, Loader2 } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { cn } from "@/lib/utils";
|
|
import { Avatar } from "@/components/crapp/Avatar";
|
|
import { Barra } from "@/components/motion/Barra";
|
|
import { giocatori, isAdmin, statoMeta, type Stato } from "@/lib/crapp-data";
|
|
import { usePresenzeEvento, useSalvaPresenza } from "@/lib/presenze";
|
|
import { useGiocatoreCorrente } from "@/lib/user-store";
|
|
|
|
const ordine: Stato[] = ["presente", "ritardo", "forse", "infortunato", "assente"];
|
|
|
|
export function RosaPresenze({ eventoId }: { eventoId: string }) {
|
|
const { risposte, isPending } = usePresenzeEvento(eventoId);
|
|
const salva = useSalvaPresenza();
|
|
const io = useGiocatoreCorrente();
|
|
const [sollecito, setSollecito] = useState(false);
|
|
|
|
const mancanti = giocatori.filter((g) => !risposte[g.id]);
|
|
const risposteN = giocatori.length - mancanti.length;
|
|
const perc = Math.round((risposteN / giocatori.length) * 100);
|
|
const daSollecitare = mancanti.length + giocatori.filter((g) => risposte[g.id] === "forse").length;
|
|
|
|
async function sollecita() {
|
|
setSollecito(true);
|
|
try {
|
|
const res = await fetch("/api/public/sollecita-presenze", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ eventoId, da: io?.nome }),
|
|
});
|
|
if (!res.ok) throw new Error();
|
|
const dati = (await res.json()) as { inviate: number; destinatari: number };
|
|
toast.success(
|
|
dati.inviate > 0
|
|
? `Sollecito inviato a ${dati.inviate} giocatori`
|
|
: `Nessuna notifica attiva tra i ${dati.destinatari} da sollecitare`,
|
|
);
|
|
} catch {
|
|
toast.error("Non sono riuscito a inviare il sollecito");
|
|
} finally {
|
|
setSollecito(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="rounded-3xl bg-card p-4 shadow-card">
|
|
<div className="flex items-baseline justify-between">
|
|
<p className="text-sm font-bold">
|
|
Hanno risposto {risposteN}/{giocatori.length}
|
|
</p>
|
|
<span className="font-display text-xl leading-none">{perc}%</span>
|
|
</div>
|
|
<Barra percentuale={perc} altezza="h-1.5" trackClassName="mt-2" />
|
|
|
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
{ordine.map((s) => {
|
|
const n = giocatori.filter((g) => risposte[g.id] === s).length;
|
|
return (
|
|
<span
|
|
key={s}
|
|
className={cn(
|
|
"rounded-full px-2.5 py-1 text-[11px] font-bold",
|
|
n > 0 ? statoMeta[s].className : "bg-secondary text-muted-foreground",
|
|
)}
|
|
>
|
|
{statoMeta[s].emoji} {n}
|
|
</span>
|
|
);
|
|
})}
|
|
<span className="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-bold text-muted-foreground">
|
|
❔ {mancanti.length} da rispondere
|
|
</span>
|
|
</div>
|
|
|
|
{io ? (
|
|
<div className="mt-4 border-t border-border pt-3">
|
|
<p className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground">
|
|
La tua risposta
|
|
</p>
|
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
{ordine.map((s) => {
|
|
const attivo = risposte[io.id] === s;
|
|
return (
|
|
<button
|
|
key={s}
|
|
type="button"
|
|
disabled={salva.isPending}
|
|
onClick={() =>
|
|
salva.mutate({ eventoId, giocatoreId: io.id, stato: attivo ? null : s })
|
|
}
|
|
className={cn(
|
|
"rounded-full border border-border px-2.5 py-1.5 text-[11px] font-semibold transition-all active:scale-95",
|
|
attivo
|
|
? cn(statoMeta[s].className, "border-transparent shadow-card")
|
|
: "bg-background text-muted-foreground",
|
|
)}
|
|
>
|
|
{statoMeta[s].label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{io && isAdmin(io.id) ? (
|
|
<button
|
|
type="button"
|
|
onClick={sollecita}
|
|
disabled={sollecito || daSollecitare === 0}
|
|
className="premi mt-4 flex w-full items-center justify-center gap-2 rounded-2xl bg-primary px-4 py-3 text-sm font-bold text-primary-foreground disabled:opacity-50"
|
|
>
|
|
{sollecito ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<BellRing className="h-4 w-4" />
|
|
)}
|
|
Sollecita {daSollecitare} giocatori
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
|
|
{isPending ? (
|
|
<p className="text-center text-xs text-muted-foreground">Carico le risposte…</p>
|
|
) : null}
|
|
|
|
{ordine.map((s) => {
|
|
const lista = giocatori.filter((g) => risposte[g.id] === s);
|
|
if (lista.length === 0) return null;
|
|
return (
|
|
<Gruppo
|
|
key={s}
|
|
titolo={`${statoMeta[s].emoji} ${statoMeta[s].label}`}
|
|
n={lista.length}
|
|
lista={lista}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{mancanti.length > 0 ? (
|
|
<Gruppo
|
|
titolo="❔ Non hanno ancora risposto"
|
|
n={mancanti.length}
|
|
lista={mancanti}
|
|
attenzione
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Gruppo({
|
|
titolo,
|
|
n,
|
|
lista,
|
|
attenzione,
|
|
}: {
|
|
titolo: string;
|
|
n: number;
|
|
lista: typeof giocatori;
|
|
attenzione?: boolean;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"rounded-3xl bg-card p-4 shadow-card",
|
|
attenzione && "border border-dashed border-warning",
|
|
)}
|
|
>
|
|
<p className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
|
{titolo} · {n}
|
|
{attenzione ? <HelpCircle className="h-3.5 w-3.5 text-warning" /> : null}
|
|
</p>
|
|
<div className="mt-3 space-y-2">
|
|
{lista.map((g) => (
|
|
<div key={g.id} className="flex items-center gap-3">
|
|
<Avatar id={g.id} fallback={String(g.numero)} className="h-8 w-8 text-xs" />
|
|
<span className="min-w-0 flex-1 truncate text-sm font-semibold">{g.nome}</span>
|
|
<span className="shrink-0 text-xs text-muted-foreground">{g.ruolo}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|