From ab63e622b2fd51f22962a086a7abda4aeaf5b942 Mon Sep 17 00:00:00 2001 From: AV77web Date: Tue, 14 Jul 2026 13:47:44 +0200 Subject: [PATCH] inserite le gestioni Dipendenti.tsx e Statistiche.tsx --- src/App.tsx | 13 +- src/pages/Assegnazioni.tsx | 11 +- src/pages/Corsi.tsx | 2 +- src/pages/Dipendenti.tsx | 172 ++++++++++++++++++++ src/pages/Statistiche.tsx | 325 +++++++++++++++++++++++++++++++++++++ 5 files changed, 512 insertions(+), 11 deletions(-) create mode 100644 src/pages/Dipendenti.tsx create mode 100644 src/pages/Statistiche.tsx diff --git a/src/App.tsx b/src/App.tsx index bc009d3..5b79b93 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,6 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { Toaster } from "@/components/ui/sonner"; import GuestRoute from "@/components/GuestRoute"; import Layout from "@/components/Layout"; -import PagePlaceholder from "@/components/PagePlaceholder"; import ProtectedRoute from "@/components/ProtectedRoute"; import { AuthProvider } from "@/context/AuthContext"; import Dashboard from "@/pages/Dashboard"; @@ -18,6 +17,8 @@ import MieiCorsi from "@/pages/MieiCorsi"; import MioCorsoDettaglio from "@/pages/MioCorsoDettaglio"; import Corsi from "@/pages/Corsi"; import Assegnazioni from "@/pages/Assegnazioni"; +import Dipendenti from "@/pages/Dipendenti"; +import Statistiche from "@/pages/Statistiche"; export default function App() { return ( @@ -63,10 +64,7 @@ export default function App() { path="/dipendenti" element={ - + } /> @@ -74,10 +72,7 @@ export default function App() { path="/statistiche" element={ - + } /> diff --git a/src/pages/Assegnazioni.tsx b/src/pages/Assegnazioni.tsx index 97e5fd6..e7f38aa 100644 --- a/src/pages/Assegnazioni.tsx +++ b/src/pages/Assegnazioni.tsx @@ -7,6 +7,7 @@ //========================================================= import { useCallback, useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import { AlertCircle, Ban, @@ -172,7 +173,7 @@ function AssegnazioneFormDialog({ ? validateAssegnazioneCreate(form) : validateAssegnazioneUpdate(form); - if (!validation.success) { + if (validation.success === false) { setFieldErrors(validation.errors); return; } @@ -327,6 +328,7 @@ function AssegnazioneFormDialog({ } export default function Assegnazioni() { + const [searchParams] = useSearchParams(); const [assegnazioni, setAssegnazioni] = useState([]); const [corsi, setCorsi] = useState([]); const [dipendenti, setDipendenti] = useState([]); @@ -346,6 +348,13 @@ export default function Assegnazioni() { ); const [actionLoading, setActionLoading] = useState(false); + useEffect(() => { + const dipendenteId = searchParams.get("dipendenteId"); + if (dipendenteId && /^\d+$/.test(dipendenteId)) { + setDipendenteFilter(dipendenteId); + } + }, [searchParams]); + useEffect(() => { Promise.all([ getCorsi(), diff --git a/src/pages/Corsi.tsx b/src/pages/Corsi.tsx index 3ac3633..59f71bb 100644 --- a/src/pages/Corsi.tsx +++ b/src/pages/Corsi.tsx @@ -139,7 +139,7 @@ function CorsoFormDialog({ const validation = mode === "create" ? validateCorsoCreate(form) : validateCorsoUpdate(form); - if (!validation.success) { + if (validation.success === false) { setFieldErrors(validation.errors); return; } diff --git a/src/pages/Dipendenti.tsx b/src/pages/Dipendenti.tsx new file mode 100644 index 0000000..7030ccc --- /dev/null +++ b/src/pages/Dipendenti.tsx @@ -0,0 +1,172 @@ +//========================================== +// 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 ( +
+ {Array.from({ length: 4 }).map((_, index) => ( + + ))} +
+ ); +} + +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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+

Dipendenti

+

+ Elenco dei dipendenti dell'organizzazione iscritti all'Academy. +

+
+ + {error && ( + + + {error} + + )} + + + + + + Ricerca + + + Filtra per nome, cognome o email. + + + +
+ + setSearch(e.target.value)} + placeholder="Es. Maria, Bianchi, academy.it" + /> +
+
+
+ + + + + + {loading ? "Caricamento..." : `${filtered.length} dipendenti`} + + + + {loading ? ( + + ) : filtered.length === 0 ? ( +

+ {search + ? "Nessun dipendente corrisponde alla ricerca." + : "Nessun dipendente registrato."} +

+ ) : ( + + + + Cognome + Nome + Email + Azioni + + + + {filtered.map((dipendente) => ( + + + {dipendente.cognome} + + {dipendente.nome} + {dipendente.email} + + + + + ))} + +
+ )} +
+
+
+ ); +} diff --git a/src/pages/Statistiche.tsx b/src/pages/Statistiche.tsx new file mode 100644 index 0000000..42f249b --- /dev/null +++ b/src/pages/Statistiche.tsx @@ -0,0 +1,325 @@ +//============================================= +// 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) { + return `${utente.cognome} ${utente.nome}`; +} + +function TableSkeleton() { + return ( +
+ {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
+ ); +} + +function SummaryCard({ + title, + value, + description, +}: { + title: string; + value: string | number; + description: string; +}) { + return ( + + + {title} + + +

{value}

+

{description}

+
+
+ ); +} + +export default function Statistiche() { + const [statistiche, setStatistiche] = useState([]); + const [categorie, setCategorie] = useState([]); + const [dipendenti, setDipendenti] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+
+

Statistiche

+

+ Riepiloghi formativi aggregati per mese e categoria. Le assegnazioni + annullate sono escluse dal conteggio. +

+
+ + {error && ( + + + {error} + + )} + + + + + + Filtri + + + Filtra per mese di assegnazione, categoria o dipendente. + + + +
+ + setMeseFilter(e.target.value)} + className="w-44" + /> +
+ +
+ + +
+ +
+ + +
+
+
+ + {!loading && statistiche.length > 0 && ( +
+ + + +
+ )} + + + + + + {loading + ? "Caricamento..." + : `${statistiche.length} righe di riepilogo`} + + {meseFilter && ( + + Dati per {formatMeseIt(meseFilter)} + {categoriaFilter !== ALL_VALUE && ` · ${categoriaFilter}`} + {dipendenteSelezionato && + ` · ${dipendenteLabel(dipendenteSelezionato)}`} + + )} + + + {loading ? ( + + ) : statistiche.length === 0 ? ( +

+ Nessun dato disponibile per i filtri selezionati. +

+ ) : ( + + + + Mese + Categoria + Assegnazioni + Completamenti + Completamento + + + + {statistiche.map((row) => ( + + {formatMeseIt(row.mese)} + {row.categoria} + + {row.numeroAssegnazioni} + + + {row.numeroCompletamenti} + + + + {row.percentualeCompletamento}% + + + + ))} + +
+ )} +
+
+
+ ); +}