Add attestato generation UI

"Genera Attestato" button on the association detail page downloads
the certificate PDF from the new backend endpoint; the edit dialog
gained a ratification-code field (used on the certificate).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017hQcuUQFue2bFvrYw2UQhK
This commit is contained in:
Linas Puplauskas 2026-08-24 17:20:28 +03:00
parent cb0c8dc140
commit 9274377a3d
2 changed files with 37 additions and 2 deletions

View File

@ -2,7 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { ArrowLeft, Loader2, Plus, Trash2 } from "lucide-react"; import { ArrowLeft, FileDown, Loader2, Plus, Trash2 } from "lucide-react";
import { AppHeader } from "@/components/layout/app-header"; import { AppHeader } from "@/components/layout/app-header";
import { PageHeader } from "@/components/layout/page-header"; import { PageHeader } from "@/components/layout/page-header";
import { AssociateFormDialog } from "@/components/associations/associate-form-dialog"; import { AssociateFormDialog } from "@/components/associations/associate-form-dialog";
@ -14,7 +14,7 @@ import { UserFormDialog } from "@/components/associations/user-form-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { deleteAssociation, getAssociation, updateAssociation } from "@/lib/api"; import { deleteAssociation, downloadAttestato, getAssociation, updateAssociation } from "@/lib/api";
import type { Associate, Association, User } from "@/lib/models"; import type { Associate, Association, User } from "@/lib/models";
export default function AssociationDetailPage() { export default function AssociationDetailPage() {
@ -31,6 +31,8 @@ export default function AssociationDetailPage() {
const [userDialogOpen, setUserDialogOpen] = useState(false); const [userDialogOpen, setUserDialogOpen] = useState(false);
const [editingUserIndex, setEditingUserIndex] = useState<number | undefined>(undefined); const [editingUserIndex, setEditingUserIndex] = useState<number | undefined>(undefined);
const [generatingAttestato, setGeneratingAttestato] = useState(false);
useEffect(() => { useEffect(() => {
getAssociation(id) getAssociation(id)
.then(setAssociation) .then(setAssociation)
@ -106,6 +108,18 @@ export default function AssociationDetailPage() {
router.push("/dashboard"); router.push("/dashboard");
}; };
const handleDownloadAttestato = async () => {
if (!association) return;
setGeneratingAttestato(true);
try {
await downloadAttestato(id, association.name);
} catch (error) {
console.error("Failed to generate attestato:", error);
} finally {
setGeneratingAttestato(false);
}
};
if (loading) { if (loading) {
return ( return (
<div className="flex min-h-screen items-center justify-center text-muted-foreground"> <div className="flex min-h-screen items-center justify-center text-muted-foreground">
@ -137,6 +151,14 @@ export default function AssociationDetailPage() {
description={`${association.associates?.length ?? 0} soci registrati`} description={`${association.associates?.length ?? 0} soci registrati`}
actions={ actions={
<> <>
<Button variant="outline" onClick={handleDownloadAttestato} disabled={generatingAttestato}>
{generatingAttestato ? (
<Loader2 className="size-4 animate-spin" />
) : (
<FileDown className="size-4" />
)}
Genera Attestato
</Button>
<EditAssociationDialog association={association} onSaved={setAssociation} /> <EditAssociationDialog association={association} onSaved={setAssociation} />
<ConfirmDialog <ConfirmDialog
trigger={ trigger={

View File

@ -30,6 +30,7 @@ export function EditAssociationDialog({
const [address, setAddress] = useState(association.address); const [address, setAddress] = useState(association.address);
const [vatNumber, setVatNumber] = useState(association.vatNumber ?? ""); const [vatNumber, setVatNumber] = useState(association.vatNumber ?? "");
const [runtsId, setRuntsId] = useState(association.runtsId ?? ""); const [runtsId, setRuntsId] = useState(association.runtsId ?? "");
const [ratificationCode, setRatificationCode] = useState(association.ratificationCode ?? "");
const [isShown, setIsShown] = useState(association.isShown ?? true); const [isShown, setIsShown] = useState(association.isShown ?? true);
useEffect(() => { useEffect(() => {
@ -38,6 +39,7 @@ export function EditAssociationDialog({
setAddress(association.address); setAddress(association.address);
setVatNumber(association.vatNumber ?? ""); setVatNumber(association.vatNumber ?? "");
setRuntsId(association.runtsId ?? ""); setRuntsId(association.runtsId ?? "");
setRatificationCode(association.ratificationCode ?? "");
setIsShown(association.isShown ?? true); setIsShown(association.isShown ?? true);
} }
}, [open, association]); }, [open, association]);
@ -52,6 +54,7 @@ export function EditAssociationDialog({
address, address,
vatNumber, vatNumber,
runtsId, runtsId,
ratificationCode,
isShown, isShown,
}); });
onSaved(updated); onSaved(updated);
@ -98,6 +101,16 @@ export function EditAssociationDialog({
<Input id="e-runtsId" value={runtsId} onChange={(e) => setRuntsId(e.target.value)} /> <Input id="e-runtsId" value={runtsId} onChange={(e) => setRuntsId(e.target.value)} />
</div> </div>
<div className="space-y-2">
<Label htmlFor="e-ratificationCode">Codice ratifica (es. LO183)</Label>
<Input
id="e-ratificationCode"
value={ratificationCode}
onChange={(e) => setRatificationCode(e.target.value)}
placeholder="Usato nell'attestato di adesione"
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-3 sm:col-span-2"> <div className="flex items-center justify-between rounded-lg border p-3 sm:col-span-2">
<div> <div>
<Label htmlFor="e-isShown">Associazione attiva</Label> <Label htmlFor="e-isShown">Associazione attiva</Label>