Prefixes the association name with "#<folder>" in the list table and the detail page title, and adds an editable field for it in the edit dialog - needed since some associations are missing it (NAS folders that were never numbered), which is what silently blanks their ratification code on the attestato. Verified live: table and detail title show "#2 ENCAP ODV", the edit field reflects the current value, and saving a new value updates both 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 Attestato
|
|
</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>
|
|
);
|
|
}
|