modifiche ad App.tsx ed index.ts
This commit is contained in:
parent
eadebfa5cd
commit
3cbbf89a06
8 changed files with 728 additions and 28 deletions
81
src/App.tsx
81
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 { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import GuestRoute from "./components/GuestRoute";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import Layout from "./components/Layout";
|
import GuestRoute from "@/components/GuestRoute";
|
||||||
import ProtectedRoute from "./components/ProtectedRoute";
|
import Layout from "@/components/Layout";
|
||||||
import { AuthProvider } from "./context/AuthContext";
|
import PagePlaceholder from "@/components/PagePlaceholder";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||||
import Login from "./pages/Login";
|
import { AuthProvider } from "@/context/AuthContext";
|
||||||
import "./App.css";
|
import Dashboard from "@/pages/Dashboard";
|
||||||
|
import Login from "@/pages/Login";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -15,10 +23,67 @@ export default function App() {
|
||||||
<Route path="/login" element={<GuestRoute><Login /></GuestRoute>} />
|
<Route path="/login" element={<GuestRoute><Login /></GuestRoute>} />
|
||||||
<Route element={<ProtectedRoute><Layout /></ProtectedRoute>}>
|
<Route element={<ProtectedRoute><Layout /></ProtectedRoute>}>
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route
|
||||||
|
path="/i-miei-corsi"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute roles={["Dipendente"]}>
|
||||||
|
<PagePlaceholder
|
||||||
|
title="I miei corsi"
|
||||||
|
description="Elenco dei corsi assegnati al dipendente."
|
||||||
|
/>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/corsi"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute roles={["Referente Academy"]}>
|
||||||
|
<PagePlaceholder
|
||||||
|
title="Catalogo corsi"
|
||||||
|
description="Gestione del catalogo corsi Academy."
|
||||||
|
/>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/assegnazioni"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute roles={["Referente Academy"]}>
|
||||||
|
<PagePlaceholder
|
||||||
|
title="Assegnazioni"
|
||||||
|
description="Gestione delle assegnazioni corsi ai dipendenti."
|
||||||
|
/>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/dipendenti"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute roles={["Referente Academy"]}>
|
||||||
|
<PagePlaceholder
|
||||||
|
title="Dipendenti"
|
||||||
|
description="Elenco dei dipendenti dell'organizzazione."
|
||||||
|
/>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/statistiche"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute roles={["Referente Academy"]}>
|
||||||
|
<PagePlaceholder
|
||||||
|
title="Statistiche"
|
||||||
|
description="Riepiloghi formativi per mese e categoria."
|
||||||
|
/>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
|
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
|
||||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
<Toaster />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import type {
|
import type {
|
||||||
|
AssegnazioneCorso,
|
||||||
|
Corso,
|
||||||
Ruolo,
|
Ruolo,
|
||||||
|
StatisticaAcademy,
|
||||||
|
StatoAssegnazione,
|
||||||
Utente,
|
Utente,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
|
|
@ -91,25 +95,90 @@ export async function logout(): Promise<void> {
|
||||||
interface MeResponse {
|
interface MeResponse {
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name?: string;
|
||||||
|
nome?: string;
|
||||||
|
cognome?: string;
|
||||||
ruolo: Ruolo;
|
ruolo: Ruolo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMe(): Promise<Utente> {
|
export async function getMe(): Promise<Utente> {
|
||||||
const data = await request<MeResponse>("/api/me");
|
const data = await request<MeResponse>("/api/me");
|
||||||
const nameParts = data.name.trim().split(/\s+/);
|
|
||||||
const nome = nameParts[0] ?? "";
|
|
||||||
const cognome = nameParts.slice(1).join(" ");
|
|
||||||
|
|
||||||
|
if (data.nome !== undefined) {
|
||||||
return {
|
return {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
nome,
|
nome: data.nome,
|
||||||
cognome,
|
cognome: data.cognome ?? "",
|
||||||
email: data.email,
|
email: data.email,
|
||||||
ruolo: data.ruolo,
|
ruolo: data.ruolo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nameParts = (data.name ?? "").trim().split(/\s+/);
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
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, string | number | undefined>): 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<Corso[]> {
|
||||||
|
return request(`/api/corsi${buildQuery({
|
||||||
|
categoria: filters?.categoria,
|
||||||
|
attivo: filters?.attivo === undefined ? undefined : filters.attivo ? "true" : "false",
|
||||||
|
})}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAssegnazioni(
|
||||||
|
filters?: AssegnazioniFilters
|
||||||
|
): Promise<AssegnazioneCorso[]> {
|
||||||
|
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<StatisticaAcademy[]> {
|
||||||
|
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: {
|
export async function register(data: {
|
||||||
nome: string;
|
nome: string;
|
||||||
cognome: string;
|
cognome: string;
|
||||||
|
|
|
||||||
78
src/components/AppSidebar.tsx
Normal file
78
src/components/AppSidebar.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<aside className="flex h-full w-64 shrink-0 flex-col border-r bg-card">
|
||||||
|
<div className="border-b px-4 py-4">
|
||||||
|
<p className="text-sm font-semibold">Academy Aziendale</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Gestione formazione</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex flex-1 flex-col gap-1 p-3">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<NavLink
|
||||||
|
key={item.to}
|
||||||
|
to={item.to}
|
||||||
|
onClick={onNavigate}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
cn(
|
||||||
|
"flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<item.icon className="size-4 shrink-0" />
|
||||||
|
{item.label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="mt-auto border-t p-4">
|
||||||
|
<div className="mb-3 space-y-1">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{user.nome} {user.cognome}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{user.email}</p>
|
||||||
|
<Badge variant="secondary" className="mt-1">
|
||||||
|
{isReferente ? "Referente Academy" : "Dipendente"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleLogout}
|
||||||
|
>
|
||||||
|
<LogOut className="size-4" />
|
||||||
|
Esci
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/components/PagePlaceholder.tsx
Normal file
40
src/components/PagePlaceholder.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-4 text-sm text-muted-foreground">
|
||||||
|
Questa sezione verrà implementata nel prossimo passo.
|
||||||
|
</p>
|
||||||
|
<Button variant="outline" onClick={() => navigate(backTo)}>
|
||||||
|
Torna alla dashboard
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,9 @@ export default function ProtectedRoute({
|
||||||
}) {
|
}) {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
if (loading) return <p className="loading">Caricamento...</p>;
|
if (loading) {
|
||||||
|
return <p className="text-sm text-muted-foreground">Caricamento...</p>;
|
||||||
|
}
|
||||||
if (!user) return <Navigate to="/login" replace />;
|
if (!user) return <Navigate to="/login" replace />;
|
||||||
if (roles && !roles.includes(user.ruolo)) {
|
if (roles && !roles.includes(user.ruolo)) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
59
src/config/navigation.ts
Normal file
59
src/config/navigation.ts
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -3,17 +3,352 @@
|
||||||
// Componete Dashborad dell'applicazione
|
// Componete Dashborad dell'applicazione
|
||||||
// author: "villari.andrea@libero.it"
|
// author: "villari.andrea@libero.it"
|
||||||
// version: "1.0.0 2026-07.14"
|
// version: "1.0.0 2026-07.14"
|
||||||
//=======================================
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import {
|
||||||
import { useAuth } from "../context/AuthContext";
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||||
|
<Icon className="size-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-2xl font-semibold">{value}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{description}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QuickLinkCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
to,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
to: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="flex flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">{title}</CardTitle>
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="mt-auto">
|
||||||
|
<Link to={to}>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Apri
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DashboardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardHeader>
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Skeleton className="mb-2 h-8 w-16" />
|
||||||
|
<Skeleton className="h-3 w-32" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DipendenteDashboard() {
|
||||||
|
const [assegnazioni, setAssegnazioni] = useState<AssegnazioneCorso[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<DashboardSkeleton />
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<StatCard
|
||||||
|
title="Corsi assegnati"
|
||||||
|
value={counts.totale}
|
||||||
|
description="Totale assegnazioni ricevute"
|
||||||
|
icon={BookOpen}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Da completare"
|
||||||
|
value={counts.assegnato}
|
||||||
|
description="Corsi ancora in corso"
|
||||||
|
icon={Clock}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Completati"
|
||||||
|
value={counts.completato}
|
||||||
|
description="Attività formative concluse"
|
||||||
|
icon={CheckCircle2}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Scaduti"
|
||||||
|
value={counts.scaduto}
|
||||||
|
description="Corsi non completati in tempo"
|
||||||
|
icon={AlertCircle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<QuickLinkCard
|
||||||
|
title="I miei corsi"
|
||||||
|
description="Visualizza, filtra e completa i corsi assegnati."
|
||||||
|
to="/i-miei-corsi"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!loading && prossimeScadenze.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Prossime scadenze</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Corsi da completare ordinati per data di scadenza.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{prossimeScadenze.map((assegnazione) => (
|
||||||
|
<div
|
||||||
|
key={assegnazione.id}
|
||||||
|
className="flex items-center justify-between gap-4 rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{assegnazione.corso.titolo}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Scadenza: {assegnazione.dataScadenza}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
assegnazione.stato === "Scaduto" ? "destructive" : "secondary"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{assegnazione.stato}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReferenteDashboard() {
|
||||||
|
const [corsiAttivi, setCorsiAttivi] = useState(0);
|
||||||
|
const [assegnazioniTotali, setAssegnazioniTotali] = useState(0);
|
||||||
|
const [completamentiMese, setCompletamentiMese] = useState(0);
|
||||||
|
const [statistiche, setStatistiche] = useState<StatisticaAcademy[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<DashboardSkeleton />
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<StatCard
|
||||||
|
title="Corsi attivi"
|
||||||
|
value={corsiAttivi}
|
||||||
|
description="Nel catalogo Academy"
|
||||||
|
icon={GraduationCap}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Assegnazioni"
|
||||||
|
value={assegnazioniTotali}
|
||||||
|
description="Totale assegnazioni nel sistema"
|
||||||
|
icon={ClipboardList}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
title="Completamenti"
|
||||||
|
value={completamentiMese}
|
||||||
|
description={`Nel mese ${currentMonth()}`}
|
||||||
|
icon={CheckCircle2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<QuickLinkCard
|
||||||
|
title="Catalogo corsi"
|
||||||
|
description="Crea e gestisci i corsi dell'Academy."
|
||||||
|
to="/corsi"
|
||||||
|
/>
|
||||||
|
<QuickLinkCard
|
||||||
|
title="Assegnazioni"
|
||||||
|
description="Assegna corsi ai dipendenti."
|
||||||
|
to="/assegnazioni"
|
||||||
|
/>
|
||||||
|
<QuickLinkCard
|
||||||
|
title="Dipendenti"
|
||||||
|
description="Consulta l'elenco dei dipendenti."
|
||||||
|
to="/dipendenti"
|
||||||
|
/>
|
||||||
|
<QuickLinkCard
|
||||||
|
title="Statistiche"
|
||||||
|
description="Riepiloghi per mese e categoria."
|
||||||
|
to="/statistiche"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!loading && statistiche.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Riepilogo del mese</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Andamento formativo per categoria ({currentMonth()}).
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{statistiche.map((row) => (
|
||||||
|
<div
|
||||||
|
key={`${row.mese}-${row.categoria}`}
|
||||||
|
className="flex items-center justify-between gap-4 rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{row.categoria}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{row.numeroCompletamenti} / {row.numeroAssegnazioni} completati
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{row.percentualeCompletamento}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isReferente = user?.ruolo === "Referente Academy";
|
|
||||||
const [deniedMessage, setDeniedMessage] = useState<string | null>(null);
|
const [deniedMessage, setDeniedMessage] = useState<string | null>(null);
|
||||||
|
const isReferente = user?.ruolo === "Referente Academy";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const denied = (location.state as { denied?: string } | null)?.denied;
|
const denied = (location.state as { denied?: string } | null)?.denied;
|
||||||
|
|
@ -24,14 +359,25 @@ export default function Dashboard() {
|
||||||
}, [location, navigate]);
|
}, [location, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="space-y-6">
|
||||||
<h1>Dashboard</h1>
|
<div>
|
||||||
{deniedMessage && <p className="error">{deniedMessage}</p>}
|
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
|
||||||
<p>
|
<p className="text-muted-foreground">
|
||||||
Benvenuto, <strong>{user?.nome} {user?.cognome}</strong>
|
Benvenuto, {user?.nome} {user?.cognome}.{" "}
|
||||||
|
{isReferente
|
||||||
|
? "Gestisci catalogo, assegnazioni e statistiche."
|
||||||
|
: "Consulta i corsi assegnati e le scadenze."}
|
||||||
</p>
|
</p>
|
||||||
<p className="muted">Ruolo: {isReferente ? "Referente Academy" : "Dipendente"}</p>
|
</div>
|
||||||
<p className="muted">Email: {user?.email}</p>
|
|
||||||
|
{deniedMessage && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle />
|
||||||
|
<AlertDescription>{deniedMessage}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isReferente ? <ReferenteDashboard /> : <DipendenteDashboard />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
//=======================================
|
//=======================================
|
||||||
export type Ruolo = "Dipendente" | "Referente Academy";
|
export type Ruolo = "Dipendente" | "Referente Academy";
|
||||||
|
|
||||||
|
export type StatoAssegnazione = "Assegnato" | "Completato" | "Scaduto" | "Annullato";
|
||||||
|
|
||||||
export interface Utente {
|
export interface Utente {
|
||||||
id: number;
|
id: number;
|
||||||
nome: string;
|
nome: string;
|
||||||
|
|
@ -14,3 +16,42 @@ export interface Utente {
|
||||||
ruolo: Ruolo;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue