From 3cbbf89a069a53ba2b0d8ca1902c8dad7e8461de Mon Sep 17 00:00:00 2001 From: AV77web Date: Tue, 14 Jul 2026 12:23:38 +0200 Subject: [PATCH] modifiche ad App.tsx ed index.ts --- src/App.tsx | 81 ++++++- src/api/client.ts | 81 ++++++- src/components/AppSidebar.tsx | 78 ++++++ src/components/PagePlaceholder.tsx | 40 ++++ src/components/ProtectedRoute.tsx | 4 +- src/config/navigation.ts | 59 +++++ src/pages/Dashboard.tsx | 372 ++++++++++++++++++++++++++++- src/types/index.ts | 41 ++++ 8 files changed, 728 insertions(+), 28 deletions(-) create mode 100644 src/components/AppSidebar.tsx create mode 100644 src/components/PagePlaceholder.tsx create mode 100644 src/config/navigation.ts diff --git a/src/App.tsx b/src/App.tsx index 4aa1e07..1bd2aaf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,19 @@ +//======================================= +// File: App.tsx +// Componete pagina principale dell'applicazione +// author: "villari.andrea@libero.it" +// version: "1.0.0 2026-07.14" +//======================================= + import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; -import GuestRoute from "./components/GuestRoute"; -import Layout from "./components/Layout"; -import ProtectedRoute from "./components/ProtectedRoute"; -import { AuthProvider } from "./context/AuthContext"; -import Dashboard from "./pages/Dashboard"; -import Login from "./pages/Login"; -import "./App.css"; +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"; +import Login from "@/pages/Login"; export default function App() { return ( @@ -15,10 +23,67 @@ export default function App() { } /> }> } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> - } /> } /> + ); diff --git a/src/api/client.ts b/src/api/client.ts index 420c24c..c67df8c 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,5 +1,9 @@ import type { + AssegnazioneCorso, + Corso, Ruolo, + StatisticaAcademy, + StatoAssegnazione, Utente, } from "../types"; @@ -91,25 +95,90 @@ export async function logout(): Promise { interface MeResponse { id: number; email: string; - name: string; + name?: string; + nome?: string; + cognome?: string; ruolo: Ruolo; } export async function getMe(): Promise { const data = await request("/api/me"); - const nameParts = data.name.trim().split(/\s+/); - const nome = nameParts[0] ?? ""; - const cognome = nameParts.slice(1).join(" "); + if (data.nome !== undefined) { + return { + id: data.id, + nome: data.nome, + cognome: data.cognome ?? "", + email: data.email, + ruolo: data.ruolo, + }; + } + + const nameParts = (data.name ?? "").trim().split(/\s+/); return { id: data.id, - nome, - cognome, + nome: nameParts[0] ?? "", + cognome: nameParts.slice(1).join(" "), email: data.email, ruolo: data.ruolo, }; } +export interface AssegnazioniFilters { + stato?: StatoAssegnazione; + categoria?: string; + corsoId?: number; + dipendenteId?: number; +} + +function buildQuery(params: Record): string { + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== "") { + search.set(key, String(value)); + } + } + const query = search.toString(); + return query ? `?${query}` : ""; +} + +export async function getCorsi(filters?: { + categoria?: string; + attivo?: boolean; +}): Promise { + return request(`/api/corsi${buildQuery({ + categoria: filters?.categoria, + attivo: filters?.attivo === undefined ? undefined : filters.attivo ? "true" : "false", + })}`); +} + +export async function getAssegnazioni( + filters?: AssegnazioniFilters +): Promise { + return request(`/api/assegnazioni-corsi${buildQuery({ + stato: filters?.stato, + categoria: filters?.categoria, + corsoId: filters?.corsoId, + dipendenteId: filters?.dipendenteId, + })}`); +} + +export async function getStatisticheAcademy(filters?: { + mese?: string; + daMese?: string; + aMese?: string; + categoria?: string; + dipendenteId?: number; +}): Promise { + return request(`/api/statistiche/academy${buildQuery({ + mese: filters?.mese, + daMese: filters?.daMese, + aMese: filters?.aMese, + categoria: filters?.categoria, + dipendenteId: filters?.dipendenteId, + })}`); +} + export async function register(data: { nome: string; cognome: string; diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx new file mode 100644 index 0000000..e237cef --- /dev/null +++ b/src/components/AppSidebar.tsx @@ -0,0 +1,78 @@ +import { NavLink, useNavigate } from "react-router-dom"; +import { LogOut } from "lucide-react"; +import { getNavItemsForRole } from "@/config/navigation"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { useAuth } from "@/context/AuthContext"; +import { cn } from "@/lib/utils"; + +interface AppSidebarProps { + onNavigate?: () => void; +} + +export default function AppSidebar({ onNavigate }: AppSidebarProps) { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + if (!user) return null; + + const navItems = getNavItemsForRole(user.ruolo); + const isReferente = user.ruolo === "Referente Academy"; + + const handleLogout = async () => { + await logout(); + navigate("/login"); + }; + + return ( + + ); +} diff --git a/src/components/PagePlaceholder.tsx b/src/components/PagePlaceholder.tsx new file mode 100644 index 0000000..8dd2ffa --- /dev/null +++ b/src/components/PagePlaceholder.tsx @@ -0,0 +1,40 @@ +import { useNavigate } from "react-router-dom"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +interface PagePlaceholderProps { + title: string; + description: string; + backTo?: string; +} + +export default function PagePlaceholder({ + title, + description, + backTo = "/dashboard", +}: PagePlaceholderProps) { + const navigate = useNavigate(); + + return ( + + + {title} + {description} + + +

+ Questa sezione verrà implementata nel prossimo passo. +

+ +
+
+ ); +} diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index a1971be..269349f 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -11,7 +11,9 @@ export default function ProtectedRoute({ }) { const { user, loading } = useAuth(); - if (loading) return

Caricamento...

; + if (loading) { + return

Caricamento...

; + } if (!user) return ; if (roles && !roles.includes(user.ruolo)) { return ( diff --git a/src/config/navigation.ts b/src/config/navigation.ts new file mode 100644 index 0000000..cf3fae5 --- /dev/null +++ b/src/config/navigation.ts @@ -0,0 +1,59 @@ +import type { LucideIcon } from "lucide-react"; +import { + BarChart3, + BookOpen, + ClipboardList, + LayoutDashboard, + Users, +} from "lucide-react"; +import type { Ruolo } from "@/types"; + +export interface NavItem { + label: string; + to: string; + icon: LucideIcon; + roles: Ruolo[]; +} + +export const NAV_ITEMS: NavItem[] = [ + { + label: "Dashboard", + to: "/dashboard", + icon: LayoutDashboard, + roles: ["Dipendente", "Referente Academy"], + }, + { + label: "I miei corsi", + to: "/i-miei-corsi", + icon: BookOpen, + roles: ["Dipendente"], + }, + { + label: "Catalogo corsi", + to: "/corsi", + icon: BookOpen, + roles: ["Referente Academy"], + }, + { + label: "Assegnazioni", + to: "/assegnazioni", + icon: ClipboardList, + roles: ["Referente Academy"], + }, + { + label: "Dipendenti", + to: "/dipendenti", + icon: Users, + roles: ["Referente Academy"], + }, + { + label: "Statistiche", + to: "/statistiche", + icon: BarChart3, + roles: ["Referente Academy"], + }, +]; + +export function getNavItemsForRole(ruolo: Ruolo): NavItem[] { + return NAV_ITEMS.filter((item) => item.roles.includes(ruolo)); +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index d63ccf7..adb5c0a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -3,17 +3,352 @@ // Componete Dashborad dell'applicazione // author: "villari.andrea@libero.it" // version: "1.0.0 2026-07.14" -//======================================= -import { useEffect, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; -import { useAuth } from "../context/AuthContext"; +import { useEffect, useMemo, useState } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import { + AlertCircle, + ArrowRight, + BookOpen, + CheckCircle2, + ClipboardList, + Clock, + GraduationCap, +} from "lucide-react"; +import { getAssegnazioni, getCorsi, getStatisticheAcademy } 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 { Skeleton } from "@/components/ui/skeleton"; +import { useAuth } from "@/context/AuthContext"; +import type { AssegnazioneCorso, StatisticaAcademy } from "@/types"; + +function currentMonth(): string { + return new Date().toISOString().slice(0, 7); +} + +function StatCard({ + title, + value, + description, + icon: Icon, +}: { + title: string; + value: number | string; + description: string; + icon: React.ComponentType<{ className?: string }>; +}) { + return ( + + + {title} + + + +

{value}

+

{description}

+
+
+ ); +} + +function QuickLinkCard({ + title, + description, + to, +}: { + title: string; + description: string; + to: string; +}) { + return ( + + + {title} + {description} + + + + + + + + ); +} + +function DashboardSkeleton() { + return ( +
+ {Array.from({ length: 4 }).map((_, index) => ( + + + + + + + + + + ))} +
+ ); +} + +function DipendenteDashboard() { + const [assegnazioni, setAssegnazioni] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + getAssegnazioni() + .then(setAssegnazioni) + .catch((err) => + setError(err instanceof Error ? err.message : "Errore nel caricamento") + ) + .finally(() => setLoading(false)); + }, []); + + const counts = useMemo( + () => ({ + totale: assegnazioni.length, + assegnato: assegnazioni.filter((a) => a.stato === "Assegnato").length, + completato: assegnazioni.filter((a) => a.stato === "Completato").length, + scaduto: assegnazioni.filter((a) => a.stato === "Scaduto").length, + }), + [assegnazioni] + ); + + const prossimeScadenze = useMemo( + () => + assegnazioni + .filter((a) => a.stato === "Assegnato" || a.stato === "Scaduto") + .sort((a, b) => a.dataScadenza.localeCompare(b.dataScadenza)) + .slice(0, 5), + [assegnazioni] + ); + + return ( +
+ {error && ( + + + {error} + + )} + + {loading ? ( + + ) : ( +
+ + + + +
+ )} + +
+ +
+ + {!loading && prossimeScadenze.length > 0 && ( + + + Prossime scadenze + + Corsi da completare ordinati per data di scadenza. + + + + {prossimeScadenze.map((assegnazione) => ( +
+
+

{assegnazione.corso.titolo}

+

+ Scadenza: {assegnazione.dataScadenza} +

+
+ + {assegnazione.stato} + +
+ ))} +
+
+ )} +
+ ); +} + +function ReferenteDashboard() { + const [corsiAttivi, setCorsiAttivi] = useState(0); + const [assegnazioniTotali, setAssegnazioniTotali] = useState(0); + const [completamentiMese, setCompletamentiMese] = useState(0); + const [statistiche, setStatistiche] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const mese = currentMonth(); + + Promise.all([ + getCorsi({ attivo: true }), + getAssegnazioni(), + getStatisticheAcademy({ mese }), + ]) + .then(([corsi, assegnazioni, stats]) => { + setCorsiAttivi(corsi.length); + setAssegnazioniTotali(assegnazioni.length); + setCompletamentiMese( + assegnazioni.filter( + (a) => + a.stato === "Completato" && + (a.dataCompletamento?.startsWith(mese) ?? false) + ).length + ); + setStatistiche(stats.slice(0, 4)); + }) + .catch((err) => + setError(err instanceof Error ? err.message : "Errore nel caricamento") + ) + .finally(() => setLoading(false)); + }, []); + + return ( +
+ {error && ( + + + {error} + + )} + + {loading ? ( + + ) : ( +
+ + + +
+ )} + +
+ + + + +
+ + {!loading && statistiche.length > 0 && ( + + + Riepilogo del mese + + Andamento formativo per categoria ({currentMonth()}). + + + + {statistiche.map((row) => ( +
+
+

{row.categoria}

+

+ {row.numeroCompletamenti} / {row.numeroAssegnazioni} completati +

+
+ + {row.percentualeCompletamento}% + +
+ ))} +
+
+ )} +
+ ); +} export default function Dashboard() { const { user } = useAuth(); const location = useLocation(); const navigate = useNavigate(); - const isReferente = user?.ruolo === "Referente Academy"; const [deniedMessage, setDeniedMessage] = useState(null); + const isReferente = user?.ruolo === "Referente Academy"; useEffect(() => { const denied = (location.state as { denied?: string } | null)?.denied; @@ -24,14 +359,25 @@ export default function Dashboard() { }, [location, navigate]); return ( -
-

Dashboard

- {deniedMessage &&

{deniedMessage}

} -

- Benvenuto, {user?.nome} {user?.cognome} -

-

Ruolo: {isReferente ? "Referente Academy" : "Dipendente"}

-

Email: {user?.email}

+
+
+

Dashboard

+

+ Benvenuto, {user?.nome} {user?.cognome}.{" "} + {isReferente + ? "Gestisci catalogo, assegnazioni e statistiche." + : "Consulta i corsi assegnati e le scadenze."} +

+
+ + {deniedMessage && ( + + + {deniedMessage} + + )} + + {isReferente ? : }
); } diff --git a/src/types/index.ts b/src/types/index.ts index 65d5d0d..28133b7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,8 @@ //======================================= export type Ruolo = "Dipendente" | "Referente Academy"; +export type StatoAssegnazione = "Assegnato" | "Completato" | "Scaduto" | "Annullato"; + export interface Utente { id: number; nome: string; @@ -14,3 +16,42 @@ export interface Utente { ruolo: Ruolo; } +export interface Corso { + id: number; + titolo: string; + descrizione: string | null; + categoria: string; + durataOre: number; + obbligatorio: boolean; + attivo: boolean; +} + +export interface AssegnazioneCorso { + id: number; + corsoId: number; + dipendenteId: number; + dataAssegnazione: string; + dataScadenza: string; + stato: StatoAssegnazione; + dataCompletamento: string | null; + corso: { + id: number; + titolo: string; + categoria: string; + durataOre: number; + }; + dipendente: { + id: number; + nome: string; + cognome: string; + email: string; + }; +} + +export interface StatisticaAcademy { + mese: string; + categoria: string; + numeroAssegnazioni: number; + numeroCompletamenti: number; + percentualeCompletamento: number; +}