ente_nazionale_frontend/app/dashboard/templates/page.tsx
Linas 5c50f28f7e Add Modelli (templates) admin section
New /dashboard/templates page (MANAGER/ADMIN, matches backend access):
lists document templates, lets MANAGER/ADMIN download the current
file and ADMIN replace it. Nav link added to the header. Also renamed
"Genera Attestato" to "Genera Certificato di Adesione" and the
downloaded file's name prefix to match, for consistency with the
document's real name.

Verified live: nav link navigates to the page, download round-trips
the file, and uploading a new file updates the listed filename and
timestamp immediately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hQcuUQFue2bFvrYw2UQhK
2026-08-24 19:22:18 +03:00

160 lines
5.3 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Download, FileText, Loader2, Upload } from "lucide-react";
import { AppHeader } from "@/components/layout/app-header";
import { PageHeader } from "@/components/layout/page-header";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useStoredUser } from "@/lib/auth";
import { downloadTemplate, getTemplates, uploadTemplate } from "@/lib/api";
import type { DocumentTemplate } from "@/lib/models";
export default function TemplatesPage() {
const router = useRouter();
const { user, ready } = useStoredUser();
const [templates, setTemplates] = useState<DocumentTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [downloadingKey, setDownloadingKey] = useState<string | null>(null);
const [uploadingKey, setUploadingKey] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const uploadTargetKey = useRef<string | null>(null);
useEffect(() => {
if (!ready) return;
if (!user || user.role === "CLIENT") {
router.push("/dashboard");
return;
}
getTemplates()
.then(setTemplates)
.catch((error: Error) => {
if (error.message === "Unauthorized") {
router.push("/login");
} else {
console.error(error);
}
})
.finally(() => setLoading(false));
}, [ready, user, router]);
const handleDownload = async (template: DocumentTemplate) => {
setDownloadingKey(template.key);
try {
await downloadTemplate(template.key, template.fileName);
} catch (error) {
console.error("Failed to download template:", error);
} finally {
setDownloadingKey(null);
}
};
const openUploadPicker = (key: string) => {
uploadTargetKey.current = key;
fileInputRef.current?.click();
};
const handleFileSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
const key = uploadTargetKey.current;
e.target.value = "";
if (!file || !key) return;
setUploadingKey(key);
try {
const updated = await uploadTemplate(key, file);
setTemplates((prev) => prev.map((t) => (t.key === key ? updated : t)));
} catch (error) {
console.error("Failed to upload template:", error);
alert("Impossibile caricare il file");
} finally {
setUploadingKey(null);
}
};
if (!ready || loading) {
return (
<div className="flex min-h-screen items-center justify-center text-muted-foreground">
<Loader2 className="size-5 animate-spin" />
</div>
);
}
if (!user || user.role === "CLIENT") {
return null;
}
return (
<div className="min-h-screen bg-muted/30">
<AppHeader />
<main className="mx-auto max-w-5xl space-y-6 p-6">
<PageHeader
title="Modelli"
description="File modello usati per generare i documenti"
/>
<input
ref={fileInputRef}
type="file"
accept="application/pdf"
className="hidden"
onChange={handleFileSelected}
/>
{templates.length === 0 ? (
<div className="rounded-lg border border-dashed py-16 text-center text-sm text-muted-foreground">
Nessun modello disponibile
</div>
) : (
templates.map((template) => (
<Card key={template.key}>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="flex items-center gap-2">
<FileText className="size-4 text-muted-foreground" />
{template.name}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-muted-foreground">
<p>{template.fileName}</p>
{template.updatedAt && (
<p>Ultimo aggiornamento: {new Date(template.updatedAt).toLocaleString("it-IT")}</p>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => handleDownload(template)}
disabled={downloadingKey === template.key}
>
{downloadingKey === template.key ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Download className="size-4" />
)}
Scarica
</Button>
{user.role === "ADMIN" && (
<Button
onClick={() => openUploadPicker(template.key)}
disabled={uploadingKey === template.key}
>
{uploadingKey === template.key ? (
<Loader2 className="size-4 animate-spin" />
) : (
<Upload className="size-4" />
)}
Sostituisci
</Button>
)}
</div>
</CardContent>
</Card>
))
)}
</main>
</div>
);
}