Compare commits
No commits in common. "7baf562a45d7a3433667690ec560d34b67cfdaae" and "4b907da5cf0fc679ad67dbc2c9171cd1561c88b6" have entirely different histories.
7baf562a45
...
4b907da5cf
5 changed files with 11 additions and 512 deletions
13
src/App.tsx
13
src/App.tsx
|
|
@ -9,6 +9,7 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import GuestRoute from "@/components/GuestRoute";
|
import GuestRoute from "@/components/GuestRoute";
|
||||||
import Layout from "@/components/Layout";
|
import Layout from "@/components/Layout";
|
||||||
|
import PagePlaceholder from "@/components/PagePlaceholder";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
import { AuthProvider } from "@/context/AuthContext";
|
||||||
import Dashboard from "@/pages/Dashboard";
|
import Dashboard from "@/pages/Dashboard";
|
||||||
|
|
@ -17,8 +18,6 @@ import MieiCorsi from "@/pages/MieiCorsi";
|
||||||
import MioCorsoDettaglio from "@/pages/MioCorsoDettaglio";
|
import MioCorsoDettaglio from "@/pages/MioCorsoDettaglio";
|
||||||
import Corsi from "@/pages/Corsi";
|
import Corsi from "@/pages/Corsi";
|
||||||
import Assegnazioni from "@/pages/Assegnazioni";
|
import Assegnazioni from "@/pages/Assegnazioni";
|
||||||
import Dipendenti from "@/pages/Dipendenti";
|
|
||||||
import Statistiche from "@/pages/Statistiche";
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -64,7 +63,10 @@ export default function App() {
|
||||||
path="/dipendenti"
|
path="/dipendenti"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute roles={["Referente Academy"]}>
|
<ProtectedRoute roles={["Referente Academy"]}>
|
||||||
<Dipendenti />
|
<PagePlaceholder
|
||||||
|
title="Dipendenti"
|
||||||
|
description="Elenco dei dipendenti dell'organizzazione."
|
||||||
|
/>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -72,7 +74,10 @@ export default function App() {
|
||||||
path="/statistiche"
|
path="/statistiche"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute roles={["Referente Academy"]}>
|
<ProtectedRoute roles={["Referente Academy"]}>
|
||||||
<Statistiche />
|
<PagePlaceholder
|
||||||
|
title="Statistiche"
|
||||||
|
description="Riepiloghi formativi per mese e categoria."
|
||||||
|
/>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
//=========================================================
|
//=========================================================
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Ban,
|
Ban,
|
||||||
|
|
@ -173,7 +172,7 @@ function AssegnazioneFormDialog({
|
||||||
? validateAssegnazioneCreate(form)
|
? validateAssegnazioneCreate(form)
|
||||||
: validateAssegnazioneUpdate(form);
|
: validateAssegnazioneUpdate(form);
|
||||||
|
|
||||||
if (validation.success === false) {
|
if (!validation.success) {
|
||||||
setFieldErrors(validation.errors);
|
setFieldErrors(validation.errors);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +327,6 @@ function AssegnazioneFormDialog({
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Assegnazioni() {
|
export default function Assegnazioni() {
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const [assegnazioni, setAssegnazioni] = useState<AssegnazioneCorso[]>([]);
|
const [assegnazioni, setAssegnazioni] = useState<AssegnazioneCorso[]>([]);
|
||||||
const [corsi, setCorsi] = useState<Corso[]>([]);
|
const [corsi, setCorsi] = useState<Corso[]>([]);
|
||||||
const [dipendenti, setDipendenti] = useState<Utente[]>([]);
|
const [dipendenti, setDipendenti] = useState<Utente[]>([]);
|
||||||
|
|
@ -348,13 +346,6 @@ export default function Assegnazioni() {
|
||||||
);
|
);
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const dipendenteId = searchParams.get("dipendenteId");
|
|
||||||
if (dipendenteId && /^\d+$/.test(dipendenteId)) {
|
|
||||||
setDipendenteFilter(dipendenteId);
|
|
||||||
}
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getCorsi(),
|
getCorsi(),
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ function CorsoFormDialog({
|
||||||
const validation =
|
const validation =
|
||||||
mode === "create" ? validateCorsoCreate(form) : validateCorsoUpdate(form);
|
mode === "create" ? validateCorsoCreate(form) : validateCorsoUpdate(form);
|
||||||
|
|
||||||
if (validation.success === false) {
|
if (!validation.success) {
|
||||||
setFieldErrors(validation.errors);
|
setFieldErrors(validation.errors);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
//==========================================
|
|
||||||
// File: Dipendenti.tsx
|
|
||||||
// Componente per la gestione dei dipendenti
|
|
||||||
// @author: "villari.andrea@libero.it"
|
|
||||||
// @version: "1.0.0 2026-07-14"
|
|
||||||
//==========================================
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { AlertCircle, ClipboardList, Search, Users } from "lucide-react";
|
|
||||||
import { getUtenti } from "@/api/client";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import type { Utente } from "@/types";
|
|
||||||
|
|
||||||
function TableSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{Array.from({ length: 4 }).map((_, index) => (
|
|
||||||
<Skeleton key={index} className="h-12 w-full" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesSearch(utente: Utente, query: string): boolean {
|
|
||||||
const normalized = query.trim().toLowerCase();
|
|
||||||
if (!normalized) return true;
|
|
||||||
|
|
||||||
return [utente.nome, utente.cognome, utente.email]
|
|
||||||
.join(" ")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dipendenti() {
|
|
||||||
const [dipendenti, setDipendenti] = useState<Utente[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
getUtenti({ ruolo: "Dipendente" })
|
|
||||||
.then(setDipendenti)
|
|
||||||
.catch((err) =>
|
|
||||||
setError(err instanceof Error ? err.message : "Errore nel caricamento")
|
|
||||||
)
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const filtered = useMemo(
|
|
||||||
() => dipendenti.filter((d) => matchesSearch(d, search)),
|
|
||||||
[dipendenti, search]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Dipendenti</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Elenco dei dipendenti dell'organizzazione iscritti all'Academy.
|
|
||||||
</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">
|
|
||||||
<Search className="size-4" />
|
|
||||||
Ricerca
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Filtra per nome, cognome o email.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="max-w-sm space-y-1.5">
|
|
||||||
<Label htmlFor="ricerca-dipendenti">Cerca dipendente</Label>
|
|
||||||
<Input
|
|
||||||
id="ricerca-dipendenti"
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
placeholder="Es. Maria, Bianchi, academy.it"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<Users className="size-4" />
|
|
||||||
{loading ? "Caricamento..." : `${filtered.length} dipendenti`}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{loading ? (
|
|
||||||
<TableSkeleton />
|
|
||||||
) : filtered.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{search
|
|
||||||
? "Nessun dipendente corrisponde alla ricerca."
|
|
||||||
: "Nessun dipendente registrato."}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Cognome</TableHead>
|
|
||||||
<TableHead>Nome</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead className="text-right">Azioni</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filtered.map((dipendente) => (
|
|
||||||
<TableRow key={dipendente.id}>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
{dipendente.cognome}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{dipendente.nome}</TableCell>
|
|
||||||
<TableCell>{dipendente.email}</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
render={
|
|
||||||
<Link
|
|
||||||
to={`/assegnazioni?dipendenteId=${dipendente.id}`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ClipboardList className="size-4" />
|
|
||||||
Assegnazioni
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,325 +0,0 @@
|
||||||
//=============================================
|
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue