inserita gestione MieiCorsi.tsx e MioCorsoDettaglio
This commit is contained in:
parent
26a97a1304
commit
d712da6295
6 changed files with 750 additions and 7 deletions
15
src/App.tsx
15
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={
|
||||
<ProtectedRoute roles={["Dipendente"]}>
|
||||
<PagePlaceholder
|
||||
title="I miei corsi"
|
||||
description="Elenco dei corsi assegnati al dipendente."
|
||||
/>
|
||||
<MieiCorsi />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/i-miei-corsi/:id"
|
||||
element={
|
||||
<ProtectedRoute roles={["Dipendente"]}>
|
||||
<MioCorsoDettaglio />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -163,6 +163,24 @@ export async function getAssegnazioni(
|
|||
})}`);
|
||||
}
|
||||
|
||||
export async function getAssegnazioneById(id: number): Promise<AssegnazioneCorso> {
|
||||
return request(`/api/assegnazioni-corsi/${id}`);
|
||||
}
|
||||
|
||||
export async function getCorsoById(id: number): Promise<Corso> {
|
||||
return request(`/api/corsi/${id}`);
|
||||
}
|
||||
|
||||
export async function completaAssegnazione(
|
||||
id: number,
|
||||
data?: { dataCompletamento?: string }
|
||||
): Promise<AssegnazioneCorso> {
|
||||
return request(`/api/assegnazioni-corsi/${id}/completa`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getStatisticheAcademy(filters?: {
|
||||
mese?: string;
|
||||
daMese?: string;
|
||||
|
|
|
|||
67
src/lib/assegnazione.ts
Normal file
67
src/lib/assegnazione.ts
Normal file
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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() {
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{prossimeScadenze.map((assegnazione) => (
|
||||
<div
|
||||
<Link
|
||||
key={assegnazione.id}
|
||||
className="flex items-center justify-between gap-4 rounded-lg border p-3"
|
||||
to={`/i-miei-corsi/${assegnazione.id}`}
|
||||
className="flex items-center justify-between gap-4 rounded-lg border p-3 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{assegnazione.corso.titolo}</p>
|
||||
|
|
@ -211,7 +213,7 @@ function DipendenteDashboard() {
|
|||
>
|
||||
{assegnazione.stato}
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
345
src/pages/MieiCorsi.tsx
Normal file
345
src/pages/MieiCorsi.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Skeleton key={index} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MieiCorsi() {
|
||||
const [assegnazioni, setAssegnazioni] = useState<AssegnazioneCorso[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [statoFilter, setStatoFilter] = useState<string>(ALL_VALUE);
|
||||
const [categoriaFilter, setCategoriaFilter] = useState<string>(ALL_VALUE);
|
||||
const [scadenzaFilter, setScadenzaFilter] = useState<ScadenzaFilter>("tutte");
|
||||
const [completaTarget, setCompletaTarget] = useState<AssegnazioneCorso | null>(
|
||||
null
|
||||
);
|
||||
const [dataCompletamento, setDataCompletamento] = useState(todayIso());
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const [categorie, setCategorie] = useState<string[]>([]);
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">I miei corsi</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Elenco dei corsi formativi assegnati a te. Filtra per stato,
|
||||
categoria o scadenza.
|
||||
</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>
|
||||
I filtri stato e categoria sono applicati dal server; la scadenza
|
||||
viene filtrata in pagina.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="filtro-stato">Stato</Label>
|
||||
<Select
|
||||
value={statoFilter}
|
||||
onValueChange={(value) => setStatoFilter(value ?? ALL_VALUE)}
|
||||
>
|
||||
<SelectTrigger id="filtro-stato" className="w-44">
|
||||
<SelectValue placeholder="Tutti gli stati" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL_VALUE}>Tutti gli stati</SelectItem>
|
||||
{STATI_ASSEGNAZIONE.map((stato) => (
|
||||
<SelectItem key={stato} value={stato}>
|
||||
{stato}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="filtro-categoria">Categoria</Label>
|
||||
<Select
|
||||
value={categoriaFilter}
|
||||
onValueChange={(value) => setCategoriaFilter(value ?? ALL_VALUE)}
|
||||
>
|
||||
<SelectTrigger id="filtro-categoria" className="w-44">
|
||||
<SelectValue placeholder="Tutte le categorie" />
|
||||
</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-scadenza">Scadenza</Label>
|
||||
<Select
|
||||
value={scadenzaFilter}
|
||||
onValueChange={(value) =>
|
||||
setScadenzaFilter((value as ScadenzaFilter) ?? "tutte")
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="filtro-scadenza" className="w-52">
|
||||
<SelectValue placeholder="Tutte" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="tutte">Tutte</SelectItem>
|
||||
<SelectItem value="prossime">
|
||||
Prossime (30 giorni)
|
||||
</SelectItem>
|
||||
<SelectItem value="scadute">Scadute</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
{loading ? "Caricamento..." : `${filtered.length} corsi`}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<TableSkeleton />
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nessun corso trovato con i filtri selezionati.
|
||||
</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Corso</TableHead>
|
||||
<TableHead>Categoria</TableHead>
|
||||
<TableHead>Assegnato il</TableHead>
|
||||
<TableHead>Scadenza</TableHead>
|
||||
<TableHead>Stato</TableHead>
|
||||
<TableHead className="text-right">Azioni</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((assegnazione) => (
|
||||
<TableRow key={assegnazione.id}>
|
||||
<TableCell className="font-medium">
|
||||
{assegnazione.corso.titolo}
|
||||
</TableCell>
|
||||
<TableCell>{assegnazione.corso.categoria}</TableCell>
|
||||
<TableCell>
|
||||
{formatDateIt(assegnazione.dataAssegnazione)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDateIt(assegnazione.dataScadenza)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statoBadgeVariant(assegnazione.stato)}>
|
||||
{assegnazione.stato}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" render={<Link to={`/i-miei-corsi/${assegnazione.id}`} />}>
|
||||
<Eye className="size-4" />
|
||||
Dettaglio
|
||||
</Button>
|
||||
{canCompletaAssegnazione(assegnazione) && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => openCompletaDialog(assegnazione)}
|
||||
>
|
||||
<CheckCircle2 className="size-4" />
|
||||
Completa
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog
|
||||
open={completaTarget !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setCompletaTarget(null);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Segna corso come completato</DialogTitle>
|
||||
<DialogDescription>
|
||||
Confermi di aver completato{" "}
|
||||
<strong>{completaTarget?.corso.titolo}</strong>?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="data-completamento">Data completamento</Label>
|
||||
<Input
|
||||
id="data-completamento"
|
||||
type="date"
|
||||
value={dataCompletamento}
|
||||
min={completaTarget?.dataAssegnazione}
|
||||
max={todayIso()}
|
||||
onChange={(e) => setDataCompletamento(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCompletaTarget(null)}
|
||||
disabled={submitting}
|
||||
>
|
||||
Annulla
|
||||
</Button>
|
||||
<Button onClick={handleCompleta} disabled={submitting}>
|
||||
{submitting ? "Salvataggio..." : "Conferma completamento"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
304
src/pages/MioCorsoDettaglio.tsx
Normal file
304
src/pages/MioCorsoDettaglio.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MioCorsoDettaglio() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [assegnazione, setAssegnazione] = useState<AssegnazioneCorso | null>(
|
||||
null
|
||||
);
|
||||
const [corso, setCorso] = useState<Corso | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 <DetailSkeleton />;
|
||||
}
|
||||
|
||||
if (error || !assegnazione) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Button variant="outline" size="sm" render={<Link to="/i-miei-corsi" />}>
|
||||
<ArrowLeft className="size-4" />
|
||||
Torna all'elenco
|
||||
</Button>
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle />
|
||||
<AlertDescription>
|
||||
{error ?? "Assegnazione non trovata"}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<Button variant="outline" size="sm" render={<Link to="/i-miei-corsi" />}>
|
||||
<ArrowLeft className="size-4" />
|
||||
Torna all'elenco
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
{assegnazione.corso.titolo}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Dettaglio dell'assegnazione formativa
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant={statoBadgeVariant(assegnazione.stato)}
|
||||
className="text-sm"
|
||||
>
|
||||
{assegnazione.stato}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<BookOpen className="size-4" />
|
||||
Informazioni corso
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Categoria</p>
|
||||
<p className="font-medium">{assegnazione.corso.categoria}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Durata</p>
|
||||
<p className="font-medium">{assegnazione.corso.durataOre} ore</p>
|
||||
</div>
|
||||
{corso?.descrizione && (
|
||||
<div>
|
||||
<p className="text-muted-foreground">Descrizione</p>
|
||||
<p>{corso.descrizione}</p>
|
||||
</div>
|
||||
)}
|
||||
{corso && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{corso.obbligatorio && (
|
||||
<Badge variant="secondary">Obbligatorio</Badge>
|
||||
)}
|
||||
{!corso.attivo && (
|
||||
<Badge variant="outline">Corso non più attivo</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Calendar className="size-4" />
|
||||
Date e stato
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Cronologia dell'assegnazione
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Data assegnazione</p>
|
||||
<p className="font-medium">
|
||||
{formatDateIt(assegnazione.dataAssegnazione)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Data scadenza</p>
|
||||
<p className="font-medium">
|
||||
{formatDateIt(assegnazione.dataScadenza)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Data completamento</p>
|
||||
<p className="font-medium">
|
||||
{formatDateIt(assegnazione.dataCompletamento)}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{canCompletaAssegnazione(assegnazione) && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Clock className="size-4" />
|
||||
Azioni
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Segna il corso come completato quando hai terminato la formazione.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={() => {
|
||||
setDataCompletamento(todayIso());
|
||||
setDialogOpen(true);
|
||||
}}>
|
||||
<CheckCircle2 className="size-4" />
|
||||
Segna come completato
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{assegnazione.stato === "Completato" && (
|
||||
<Alert>
|
||||
<CheckCircle2 />
|
||||
<AlertDescription>
|
||||
Hai completato questo corso il{" "}
|
||||
{formatDateIt(assegnazione.dataCompletamento)}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Conferma completamento</DialogTitle>
|
||||
<DialogDescription>
|
||||
Indica la data in cui hai completato la formazione su{" "}
|
||||
<strong>{assegnazione.corso.titolo}</strong>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="dettaglio-data-completamento">
|
||||
Data completamento
|
||||
</Label>
|
||||
<Input
|
||||
id="dettaglio-data-completamento"
|
||||
type="date"
|
||||
value={dataCompletamento}
|
||||
min={assegnazione.dataAssegnazione}
|
||||
max={todayIso()}
|
||||
onChange={(e) => setDataCompletamento(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
disabled={submitting}
|
||||
>
|
||||
Annulla
|
||||
</Button>
|
||||
<Button onClick={handleCompleta} disabled={submitting}>
|
||||
{submitting ? "Salvataggio..." : "Conferma"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue