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
228 lines
7.8 KiB
TypeScript
228 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { ArrowLeft, FileDown, Loader2, Plus, Trash2 } from "lucide-react";
|
|
import { AppHeader } from "@/components/layout/app-header";
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
import { AssociateFormDialog } from "@/components/associations/associate-form-dialog";
|
|
import { AssociateTable } from "@/components/associations/associate-table";
|
|
import { AssociationInfoCard } from "@/components/associations/association-info-card";
|
|
import { EditAssociationDialog } from "@/components/associations/edit-association-dialog";
|
|
import { UserAccountsCard } from "@/components/associations/user-accounts-card";
|
|
import { UserFormDialog } from "@/components/associations/user-form-dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
|
import { deleteAssociation, downloadAttestato, getAssociation, updateAssociation } from "@/lib/api";
|
|
import type { Associate, Association, User } from "@/lib/models";
|
|
|
|
export default function AssociationDetailPage() {
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const id = params.id as string;
|
|
|
|
const [association, setAssociation] = useState<Association | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [associateDialogOpen, setAssociateDialogOpen] = useState(false);
|
|
const [editingAssociateIndex, setEditingAssociateIndex] = useState<number | undefined>(undefined);
|
|
|
|
const [userDialogOpen, setUserDialogOpen] = useState(false);
|
|
const [editingUserIndex, setEditingUserIndex] = useState<number | undefined>(undefined);
|
|
|
|
const [generatingAttestato, setGeneratingAttestato] = useState(false);
|
|
|
|
useEffect(() => {
|
|
getAssociation(id)
|
|
.then(setAssociation)
|
|
.catch((error) => {
|
|
console.error("Failed to load association:", error);
|
|
router.push("/dashboard");
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, [id, router]);
|
|
|
|
const persist = async (updated: Association) => {
|
|
const saved = await updateAssociation(id, updated);
|
|
setAssociation(saved);
|
|
};
|
|
|
|
const openAddAssociate = () => {
|
|
setEditingAssociateIndex(undefined);
|
|
setAssociateDialogOpen(true);
|
|
};
|
|
|
|
const openEditAssociate = (index: number) => {
|
|
setEditingAssociateIndex(index);
|
|
setAssociateDialogOpen(true);
|
|
};
|
|
|
|
const handleAssociateSubmit = async (associate: Associate) => {
|
|
if (!association) return;
|
|
const associates = [...(association.associates ?? [])];
|
|
if (editingAssociateIndex !== undefined) {
|
|
associates[editingAssociateIndex] = associate;
|
|
} else {
|
|
associates.push(associate);
|
|
}
|
|
await persist({ ...association, associates });
|
|
};
|
|
|
|
const handleAssociateDelete = async (index: number) => {
|
|
if (!association) return;
|
|
const associates = (association.associates ?? []).filter((_, i) => i !== index);
|
|
await persist({ ...association, associates });
|
|
};
|
|
|
|
const openAddUser = () => {
|
|
setEditingUserIndex(undefined);
|
|
setUserDialogOpen(true);
|
|
};
|
|
|
|
const openEditUser = (index: number) => {
|
|
setEditingUserIndex(index);
|
|
setUserDialogOpen(true);
|
|
};
|
|
|
|
const handleUserSubmit = async (user: User) => {
|
|
if (!association) return;
|
|
const users = [...(association.users ?? [])];
|
|
if (editingUserIndex !== undefined) {
|
|
const existing = users[editingUserIndex];
|
|
users[editingUserIndex] = { ...user, password: user.password || existing.password };
|
|
} else {
|
|
users.push(user);
|
|
}
|
|
await persist({ ...association, users });
|
|
};
|
|
|
|
const handleUserDelete = async (index: number) => {
|
|
if (!association) return;
|
|
const users = (association.users ?? []).filter((_, i) => i !== index);
|
|
await persist({ ...association, users });
|
|
};
|
|
|
|
const handleDeleteAssociation = async () => {
|
|
await deleteAssociation(id);
|
|
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) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center text-muted-foreground">
|
|
<Loader2 className="size-5 animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!association) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-muted/30">
|
|
<AppHeader />
|
|
<main className="mx-auto max-w-5xl space-y-6 p-6">
|
|
<div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="-ml-2 mb-2"
|
|
onClick={() => router.push("/dashboard")}
|
|
>
|
|
<ArrowLeft className="size-4" />
|
|
Torna alle associazioni
|
|
</Button>
|
|
<PageHeader
|
|
title={association.nasFolderNumber != null ? `#${association.nasFolderNumber} ${association.name}` : association.name}
|
|
description={`${association.associates?.length ?? 0} soci registrati`}
|
|
actions={
|
|
<>
|
|
<Button variant="outline" onClick={handleDownloadAttestato} disabled={generatingAttestato}>
|
|
{generatingAttestato ? (
|
|
<Loader2 className="size-4 animate-spin" />
|
|
) : (
|
|
<FileDown className="size-4" />
|
|
)}
|
|
Genera Certificato di Adesione
|
|
</Button>
|
|
<EditAssociationDialog association={association} onSaved={setAssociation} />
|
|
<ConfirmDialog
|
|
trigger={
|
|
<Button variant="outline" className="text-destructive hover:text-destructive">
|
|
<Trash2 className="size-4" />
|
|
Elimina
|
|
</Button>
|
|
}
|
|
title="Eliminare questa associazione?"
|
|
description={`"${association.name}" e tutti i suoi soci verranno eliminati definitivamente. L'operazione non è reversibile.`}
|
|
confirmLabel="Elimina definitivamente"
|
|
onConfirm={handleDeleteAssociation}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<AssociationInfoCard association={association} />
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Soci ({association.associates?.length ?? 0})</CardTitle>
|
|
<Button size="sm" onClick={openAddAssociate}>
|
|
<Plus className="size-4" />
|
|
Aggiungi socio
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<AssociateTable
|
|
associates={association.associates ?? []}
|
|
onEdit={openEditAssociate}
|
|
onDelete={handleAssociateDelete}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<UserAccountsCard
|
|
users={association.users ?? []}
|
|
onEdit={openEditUser}
|
|
onDelete={handleUserDelete}
|
|
actions={
|
|
<Button size="sm" onClick={openAddUser}>
|
|
<Plus className="size-4" />
|
|
Aggiungi account
|
|
</Button>
|
|
}
|
|
/>
|
|
</main>
|
|
|
|
<AssociateFormDialog
|
|
open={associateDialogOpen}
|
|
onOpenChange={setAssociateDialogOpen}
|
|
initialValue={editingAssociateIndex !== undefined ? association.associates?.[editingAssociateIndex] : undefined}
|
|
onSubmit={handleAssociateSubmit}
|
|
/>
|
|
|
|
<UserFormDialog
|
|
open={userDialogOpen}
|
|
onOpenChange={setUserDialogOpen}
|
|
initialValue={editingUserIndex !== undefined ? association.users?.[editingUserIndex] : undefined}
|
|
onSubmit={handleUserSubmit}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|