esameits24-26frontend/src/pages/Statistiche.tsx
AV77web af35df94d3
All checks were successful
Deploy to VPS / build (push) Successful in 30s
Deploy to VPS / deploy (push) Successful in 23s
corretti i commenti iniziali dei file
2026-07-14 15:09:47 +02:00

325 lines
10 KiB
TypeScript

//=============================================
// File: Statistiche.tsx
// Componente per la gestione delle statistiche
// da parte dei Referenti Academy
// @author: "villari.andrea@libero.it"
// @version: "1.0.0 2026-07-14"
//=============================================
import { useCallback, useEffect, useMemo, useState } from "react";
import { AlertCircle, BarChart3, Filter } from "lucide-react";
import { getCorsi, getStatisticheAcademy, getUtenti } from "@/api/client";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { StatisticaAcademy, Utente } from "@/types";
const ALL_VALUE = "__all__";
function currentMonth(): string {
return new Date().toISOString().slice(0, 7);
}
function formatMeseIt(mese: string): string {
const [year, month] = mese.split("-");
if (!year || !month) return mese;
const date = new Date(Number(year), Number(month) - 1, 1);
return date.toLocaleDateString("it-IT", { month: "long", year: "numeric" });
}
function dipendenteLabel(utente: Pick<Utente, "nome" | "cognome">) {
return `${utente.cognome} ${utente.nome}`;
}
function TableSkeleton() {
return (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, index) => (
<Skeleton key={index} className="h-12 w-full" />
))}
</div>
);
}
function SummaryCard({
title,
value,
description,
}: {
title: string;
value: string | number;
description: string;
}) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-semibold">{value}</p>
<p className="text-xs text-muted-foreground">{description}</p>
</CardContent>
</Card>
);
}
export default function Statistiche() {
const [statistiche, setStatistiche] = useState<StatisticaAcademy[]>([]);
const [categorie, setCategorie] = useState<string[]>([]);
const [dipendenti, setDipendenti] = useState<Utente[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [meseFilter, setMeseFilter] = useState(currentMonth());
const [categoriaFilter, setCategoriaFilter] = useState(ALL_VALUE);
const [dipendenteFilter, setDipendenteFilter] = useState(ALL_VALUE);
useEffect(() => {
Promise.all([getCorsi(), getUtenti({ ruolo: "Dipendente" })])
.then(([corsi, dipendentiData]) => {
const values = new Set(corsi.map((c) => c.categoria));
setCategorie(Array.from(values).sort());
setDipendenti(dipendentiData);
})
.catch(() => {
/* i filtri possono restare vuoti */
});
}, []);
const loadStatistiche = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await getStatisticheAcademy({
mese: meseFilter || undefined,
categoria: categoriaFilter === ALL_VALUE ? undefined : categoriaFilter,
dipendenteId:
dipendenteFilter === ALL_VALUE
? undefined
: Number(dipendenteFilter),
});
setStatistiche(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Errore nel caricamento");
} finally {
setLoading(false);
}
}, [meseFilter, categoriaFilter, dipendenteFilter]);
useEffect(() => {
loadStatistiche();
}, [loadStatistiche]);
const totali = useMemo(() => {
const assegnazioni = statistiche.reduce(
(sum, row) => sum + row.numeroAssegnazioni,
0
);
const completamenti = statistiche.reduce(
(sum, row) => sum + row.numeroCompletamenti,
0
);
const percentuale =
assegnazioni === 0
? 0
: Math.round((completamenti / assegnazioni) * 10000) / 100;
return { assegnazioni, completamenti, percentuale };
}, [statistiche]);
const dipendenteSelezionato = dipendenti.find(
(d) => String(d.id) === dipendenteFilter
);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Statistiche</h1>
<p className="text-muted-foreground">
Riepiloghi formativi aggregati per mese e categoria. Le assegnazioni
annullate sono escluse dal conteggio.
</p>
</div>
{error && (
<Alert variant="destructive">
<AlertCircle />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Card>
<CardHeader className="pb-4">
<CardTitle className="flex items-center gap-2 text-base">
<Filter className="size-4" />
Filtri
</CardTitle>
<CardDescription>
Filtra per mese di assegnazione, categoria o dipendente.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-4">
<div className="space-y-1.5">
<Label htmlFor="filtro-mese-statistiche">Mese</Label>
<Input
id="filtro-mese-statistiche"
type="month"
value={meseFilter}
onChange={(e) => setMeseFilter(e.target.value)}
className="w-44"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="filtro-categoria-statistiche">Categoria</Label>
<Select
value={categoriaFilter}
onValueChange={(value) => setCategoriaFilter(value ?? ALL_VALUE)}
>
<SelectTrigger id="filtro-categoria-statistiche" className="w-44">
<SelectValue placeholder="Tutte" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_VALUE}>Tutte le categorie</SelectItem>
{categorie.map((categoria) => (
<SelectItem key={categoria} value={categoria}>
{categoria}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor="filtro-dipendente-statistiche">Dipendente</Label>
<Select
value={dipendenteFilter}
onValueChange={(value) => setDipendenteFilter(value ?? ALL_VALUE)}
>
<SelectTrigger
id="filtro-dipendente-statistiche"
className="w-52"
>
<SelectValue placeholder="Tutti" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_VALUE}>Tutti i dipendenti</SelectItem>
{dipendenti.map((dipendente) => (
<SelectItem key={dipendente.id} value={String(dipendente.id)}>
{dipendenteLabel(dipendente)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{!loading && statistiche.length > 0 && (
<div className="grid gap-4 sm:grid-cols-3">
<SummaryCard
title="Assegnazioni"
value={totali.assegnazioni}
description={
meseFilter
? `Nel mese ${formatMeseIt(meseFilter)}`
: "Totale nel periodo"
}
/>
<SummaryCard
title="Completamenti"
value={totali.completamenti}
description="Corsi conclusi con successo"
/>
<SummaryCard
title="Tasso completamento"
value={`${totali.percentuale}%`}
description="Media sulle righe filtrate"
/>
</div>
)}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<BarChart3 className="size-4" />
{loading
? "Caricamento..."
: `${statistiche.length} righe di riepilogo`}
</CardTitle>
{meseFilter && (
<CardDescription>
Dati per {formatMeseIt(meseFilter)}
{categoriaFilter !== ALL_VALUE && ` · ${categoriaFilter}`}
{dipendenteSelezionato &&
` · ${dipendenteLabel(dipendenteSelezionato)}`}
</CardDescription>
)}
</CardHeader>
<CardContent>
{loading ? (
<TableSkeleton />
) : statistiche.length === 0 ? (
<p className="text-sm text-muted-foreground">
Nessun dato disponibile per i filtri selezionati.
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Mese</TableHead>
<TableHead>Categoria</TableHead>
<TableHead className="text-right">Assegnazioni</TableHead>
<TableHead className="text-right">Completamenti</TableHead>
<TableHead className="text-right">Completamento</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{statistiche.map((row) => (
<TableRow key={`${row.mese}-${row.categoria}`}>
<TableCell>{formatMeseIt(row.mese)}</TableCell>
<TableCell className="font-medium">{row.categoria}</TableCell>
<TableCell className="text-right">
{row.numeroAssegnazioni}
</TableCell>
<TableCell className="text-right">
{row.numeroCompletamenti}
</TableCell>
<TableCell className="text-right">
<Badge variant="secondary">
{row.percentualeCompletamento}%
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}