From d712da6295f26e6c4124b333ef7d23c8c0a44841 Mon Sep 17 00:00:00 2001 From: AV77web Date: Tue, 14 Jul 2026 13:09:01 +0200 Subject: [PATCH] inserita gestione MieiCorsi.tsx e MioCorsoDettaglio --- src/App.tsx | 15 +- src/api/client.ts | 18 ++ src/lib/assegnazione.ts | 67 +++++++ src/pages/Dashboard.tsx | 8 +- src/pages/MieiCorsi.tsx | 345 ++++++++++++++++++++++++++++++++ src/pages/MioCorsoDettaglio.tsx | 304 ++++++++++++++++++++++++++++ 6 files changed, 750 insertions(+), 7 deletions(-) create mode 100644 src/lib/assegnazione.ts create mode 100644 src/pages/MieiCorsi.tsx create mode 100644 src/pages/MioCorsoDettaglio.tsx diff --git a/src/App.tsx b/src/App.tsx index 1bd2aaf..5be2a98 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,8 @@ import ProtectedRoute from "@/components/ProtectedRoute"; import { AuthProvider } from "@/context/AuthContext"; import Dashboard from "@/pages/Dashboard"; import Login from "@/pages/Login"; +import MieiCorsi from "@/pages/MieiCorsi"; +import MioCorsoDettaglio from "@/pages/MioCorsoDettaglio"; export default function App() { return ( @@ -27,10 +29,15 @@ export default function App() { path="/i-miei-corsi" element={ - + + + } + /> + + } /> diff --git a/src/api/client.ts b/src/api/client.ts index c67df8c..c5c8589 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -163,6 +163,24 @@ export async function getAssegnazioni( })}`); } +export async function getAssegnazioneById(id: number): Promise { + return request(`/api/assegnazioni-corsi/${id}`); +} + +export async function getCorsoById(id: number): Promise { + return request(`/api/corsi/${id}`); +} + +export async function completaAssegnazione( + id: number, + data?: { dataCompletamento?: string } +): Promise { + return request(`/api/assegnazioni-corsi/${id}/completa`, { + method: "PUT", + body: JSON.stringify(data ?? {}), + }); +} + export async function getStatisticheAcademy(filters?: { mese?: string; daMese?: string; diff --git a/src/lib/assegnazione.ts b/src/lib/assegnazione.ts new file mode 100644 index 0000000..ff9dbca --- /dev/null +++ b/src/lib/assegnazione.ts @@ -0,0 +1,67 @@ +//============================================== +// File: assegnazione.ts +// Componente per la gestione delle Assegnazioni +// dei corso ai dipendenti +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//============================================== +import type { StatoAssegnazione, AssegnazioneCorso } from "@/types"; + +export type ScadenzaFilter = "tutte" | "prossime" | "scadute"; + +export const STATI_ASSEGNAZIONE: StatoAssegnazione[] = [ + "Assegnato", + "Completato", + "Scaduto", + "Annullato", +]; + +export function formatDateIt(value: string | null | undefined): string { + if (!value) return "—"; + const [year, month, day] = value.split("-"); + if (!year || !month || !day) return value; + return `${day}/${month}/${year}`; +} + +export function todayIso(): string { + return new Date().toISOString().slice(0, 10); +} + +export function statoBadgeVariant( + stato: StatoAssegnazione +): "default" | "secondary" | "destructive" | "outline" { + switch (stato) { + case "Completato": + return "default"; + case "Assegnato": + return "secondary"; + case "Scaduto": + return "destructive"; + case "Annullato": + return "outline"; + } +} + +export function canCompletaAssegnazione(assegnazione: AssegnazioneCorso): boolean { + return assegnazione.stato === "Assegnato" || assegnazione.stato === "Scaduto"; +} + +export function matchesScadenzaFilter( + assegnazione: AssegnazioneCorso, + filter: ScadenzaFilter +): boolean { + if (filter === "tutte") return true; + if (filter === "scadute") return assegnazione.stato === "Scaduto"; + if (filter !== "prossime") return true; + + if (assegnazione.stato !== "Assegnato") return false; + + const today = todayIso(); + const limit = new Date(); + limit.setDate(limit.getDate() + 30); + const limitIso = limit.toISOString().slice(0, 10); + + return ( + assegnazione.dataScadenza >= today && assegnazione.dataScadenza <= limitIso + ); +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index adb5c0a..3f77ece 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -3,6 +3,7 @@ // Componete Dashborad dell'applicazione // author: "villari.andrea@libero.it" // version: "1.0.0 2026-07.14" +//====================================== import { useEffect, useMemo, useState } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { @@ -194,9 +195,10 @@ function DipendenteDashboard() { {prossimeScadenze.map((assegnazione) => ( -

{assegnazione.corso.titolo}

@@ -211,7 +213,7 @@ function DipendenteDashboard() { > {assegnazione.stato} -
+ ))} diff --git a/src/pages/MieiCorsi.tsx b/src/pages/MieiCorsi.tsx new file mode 100644 index 0000000..0b61344 --- /dev/null +++ b/src/pages/MieiCorsi.tsx @@ -0,0 +1,345 @@ +//===================================== +// File: MieiCorsi.tsx +// componente tabella con tutti i corsi +// assegnati al dipendente. +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//===================================== + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Link } from "react-router-dom"; +import { AlertCircle, CheckCircle2, Eye, Filter } from "lucide-react"; +import { toast } from "sonner"; +import { completaAssegnazione, getAssegnazioni } from "@/api/client"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +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 { + canCompletaAssegnazione, + formatDateIt, + matchesScadenzaFilter, + STATI_ASSEGNAZIONE, + statoBadgeVariant, + todayIso, + type ScadenzaFilter, +} from "@/lib/assegnazione"; +import type { AssegnazioneCorso, StatoAssegnazione } from "@/types"; + +const ALL_VALUE = "__all__"; + +function TableSkeleton() { + return ( +
+ {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
+ ); +} + +export default function MieiCorsi() { + const [assegnazioni, setAssegnazioni] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [statoFilter, setStatoFilter] = useState(ALL_VALUE); + const [categoriaFilter, setCategoriaFilter] = useState(ALL_VALUE); + const [scadenzaFilter, setScadenzaFilter] = useState("tutte"); + const [completaTarget, setCompletaTarget] = useState( + null + ); + const [dataCompletamento, setDataCompletamento] = useState(todayIso()); + const [submitting, setSubmitting] = useState(false); + + const [categorie, setCategorie] = useState([]); + + const loadAssegnazioni = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await getAssegnazioni({ + stato: + statoFilter === ALL_VALUE + ? undefined + : (statoFilter as StatoAssegnazione), + categoria: categoriaFilter === ALL_VALUE ? undefined : categoriaFilter, + }); + setAssegnazioni(data); + + if (statoFilter === ALL_VALUE && categoriaFilter === ALL_VALUE) { + const values = new Set(data.map((a) => a.corso.categoria)); + setCategorie(Array.from(values).sort()); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Errore nel caricamento"); + } finally { + setLoading(false); + } + }, [statoFilter, categoriaFilter]); + + useEffect(() => { + loadAssegnazioni(); + }, [loadAssegnazioni]); + + const filtered = useMemo( + () => assegnazioni.filter((a) => matchesScadenzaFilter(a, scadenzaFilter)), + [assegnazioni, scadenzaFilter] + ); + + function openCompletaDialog(assegnazione: AssegnazioneCorso) { + setDataCompletamento(todayIso()); + setCompletaTarget(assegnazione); + } + + async function handleCompleta() { + if (!completaTarget) return; + + setSubmitting(true); + try { + await completaAssegnazione(completaTarget.id, { dataCompletamento }); + toast.success("Corso segnato come completato"); + setCompletaTarget(null); + await loadAssegnazioni(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Impossibile completare il corso" + ); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+

I miei corsi

+

+ Elenco dei corsi formativi assegnati a te. Filtra per stato, + categoria o scadenza. +

+
+ + {error && ( + + + {error} + + )} + + + + + + Filtri + + + I filtri stato e categoria sono applicati dal server; la scadenza + viene filtrata in pagina. + + + +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + + + {loading ? "Caricamento..." : `${filtered.length} corsi`} + + + + {loading ? ( + + ) : filtered.length === 0 ? ( +

+ Nessun corso trovato con i filtri selezionati. +

+ ) : ( + + + + Corso + Categoria + Assegnato il + Scadenza + Stato + Azioni + + + + {filtered.map((assegnazione) => ( + + + {assegnazione.corso.titolo} + + {assegnazione.corso.categoria} + + {formatDateIt(assegnazione.dataAssegnazione)} + + + {formatDateIt(assegnazione.dataScadenza)} + + + + {assegnazione.stato} + + + +
+ + {canCompletaAssegnazione(assegnazione) && ( + + )} +
+
+
+ ))} +
+
+ )} +
+
+ + { + if (!open) setCompletaTarget(null); + }} + > + + + Segna corso come completato + + Confermi di aver completato{" "} + {completaTarget?.corso.titolo}? + + +
+ + setDataCompletamento(e.target.value)} + /> +
+ + + + +
+
+
+ ); +} diff --git a/src/pages/MioCorsoDettaglio.tsx b/src/pages/MioCorsoDettaglio.tsx new file mode 100644 index 0000000..be63d58 --- /dev/null +++ b/src/pages/MioCorsoDettaglio.tsx @@ -0,0 +1,304 @@ +//==================================== +// File: MioCorsoDettaglio.tsx +// componenete con il dettaglio corso +// @author: "villari.andrea@libero.it" +// @version: "1.0.0 2026-07-14" +//==================================== + +import { useEffect, useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { + AlertCircle, + ArrowLeft, + BookOpen, + Calendar, + CheckCircle2, + Clock, +} from "lucide-react"; +import { toast } from "sonner"; +import { + completaAssegnazione, + getAssegnazioneById, + getCorsoById, +} from "@/api/client"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + canCompletaAssegnazione, + formatDateIt, + statoBadgeVariant, + todayIso, +} from "@/lib/assegnazione"; +import type { AssegnazioneCorso, Corso } from "@/types"; + +function DetailSkeleton() { + return ( +
+ + + +
+ ); +} + +export default function MioCorsoDettaglio() { + const { id } = useParams<{ id: string }>(); + const [assegnazione, setAssegnazione] = useState( + null + ); + const [corso, setCorso] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [dataCompletamento, setDataCompletamento] = useState(todayIso()); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + const assegnazioneId = Number(id); + if (!Number.isInteger(assegnazioneId) || assegnazioneId <= 0) { + setError("Identificativo assegnazione non valido"); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + getAssegnazioneById(assegnazioneId) + .then(async (data) => { + setAssegnazione(data); + const corsoData = await getCorsoById(data.corsoId); + setCorso(corsoData); + }) + .catch((err) => { + setError(err instanceof Error ? err.message : "Errore nel caricamento"); + }) + .finally(() => setLoading(false)); + }, [id]); + + async function handleCompleta() { + if (!assegnazione) return; + + setSubmitting(true); + try { + const updated = await completaAssegnazione(assegnazione.id, { + dataCompletamento, + }); + setAssegnazione(updated); + setDialogOpen(false); + toast.success("Corso segnato come completato"); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Impossibile completare il corso" + ); + } finally { + setSubmitting(false); + } + } + + if (loading) { + return ; + } + + if (error || !assegnazione) { + return ( +
+ + + + + {error ?? "Assegnazione non trovata"} + + +
+ ); + } + + return ( +
+
+
+ +
+

+ {assegnazione.corso.titolo} +

+

+ Dettaglio dell'assegnazione formativa +

+
+
+ + {assegnazione.stato} + +
+ +
+ + + + + Informazioni corso + + + +
+

Categoria

+

{assegnazione.corso.categoria}

+
+
+

Durata

+

{assegnazione.corso.durataOre} ore

+
+ {corso?.descrizione && ( +
+

Descrizione

+

{corso.descrizione}

+
+ )} + {corso && ( +
+ {corso.obbligatorio && ( + Obbligatorio + )} + {!corso.attivo && ( + Corso non più attivo + )} +
+ )} +
+
+ + + + + + Date e stato + + + Cronologia dell'assegnazione + + + +
+

Data assegnazione

+

+ {formatDateIt(assegnazione.dataAssegnazione)} +

+
+
+

Data scadenza

+

+ {formatDateIt(assegnazione.dataScadenza)} +

+
+
+

Data completamento

+

+ {formatDateIt(assegnazione.dataCompletamento)} +

+
+
+
+
+ + {canCompletaAssegnazione(assegnazione) && ( + + + + + Azioni + + + Segna il corso come completato quando hai terminato la formazione. + + + + + + + )} + + {assegnazione.stato === "Completato" && ( + + + + Hai completato questo corso il{" "} + {formatDateIt(assegnazione.dataCompletamento)}. + + + )} + + + + + Conferma completamento + + Indica la data in cui hai completato la formazione su{" "} + {assegnazione.corso.titolo}. + + +
+ + setDataCompletamento(e.target.value)} + /> +
+ + + + +
+
+
+ ); +}