Checkpoint: Ajout de l'association Service→Signature dans l'onglet Signatures des Paramètres et apposition automatique de la signature sur les PDFs exportés lors de l'export BAP. Inclut : table serviceSignatures, routes tRPC, interface de configuration, et modification de exportToPdf pour apposer la signature en bas à droite du PDF avec le nom du signataire.

This commit is contained in:
Manus
2026-03-14 19:28:50 -04:00
parent cf886a1adb
commit ee49992530
8 changed files with 1790 additions and 6 deletions

View File

@@ -402,10 +402,155 @@ function SignaturesSection() {
)}
</CardContent>
</Card>
{/* Service → Signature associations */}
<ServiceSignaturesSection signatures={signatures || []} />
</div>
);
}
function ServiceSignaturesSection({ signatures }: { signatures: Array<{ id: number; firstName: string; lastName: string; imageUrl: string }> }) {
const utils = trpc.useUtils();
const { data: departments, isLoading: depsLoading } = trpc.departments.getByUser.useQuery();
const { data: associations, isLoading: assocLoading } = trpc.serviceSignatures.list.useQuery();
const upsertMutation = trpc.serviceSignatures.upsert.useMutation({
onSuccess: () => {
toast.success("Association enregistrée");
utils.serviceSignatures.list.invalidate();
},
onError: (e) => toast.error(e.message || "Erreur lors de l'enregistrement"),
});
const deleteMutation = trpc.serviceSignatures.delete.useMutation({
onSuccess: () => {
toast.success("Association supprimée");
utils.serviceSignatures.list.invalidate();
},
onError: (e) => toast.error(e.message || "Erreur lors de la suppression"),
});
const getAssociation = (serviceName: string) =>
associations?.find(a => a.serviceName.toLowerCase() === serviceName.toLowerCase());
if (depsLoading || assocLoading) {
return (
<Card>
<CardHeader><CardTitle className="flex items-center gap-2"><User className="w-5 h-5 text-emerald-500" />Association Service → Signature</CardTitle></CardHeader>
<CardContent><div className="flex justify-center py-6"><Loader2 className="w-6 h-6 animate-spin text-primary" /></div></CardContent>
</Card>
);
}
if (!departments || departments.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2"><User className="w-5 h-5 text-emerald-500" />Association Service → Signature</CardTitle>
<CardDescription>Associez une signature à chaque service pour l'apposer automatiquement lors de l'export BAP</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground text-center py-4">Aucun service configuré. Ajoutez des services dans l'onglet <strong>Listes</strong> d'abord.</p>
</CardContent>
</Card>
);
}
if (!signatures || signatures.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2"><User className="w-5 h-5 text-emerald-500" />Association Service → Signature</CardTitle>
<CardDescription>Associez une signature à chaque service pour l'apposer automatiquement lors de l'export BAP</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground text-center py-4">Aucune signature enregistrée. Ajoutez des signatures ci-dessus d'abord.</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2"><User className="w-5 h-5 text-emerald-500" />Association Service → Signature</CardTitle>
<CardDescription>Associez une signature à chaque service pour l'apposer automatiquement lors de l'export PDF BAP</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Service</TableHead>
<TableHead>Signature associée</TableHead>
<TableHead>Aperçu</TableHead>
<TableHead className="w-20">Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{departments.map((dept: { id: number; name: string; userId: number; createdAt: Date }) => {
const assoc = getAssociation(dept.name);
const currentSigId = assoc?.signatureId ?? 0;
return (
<TableRow key={dept.id}>
<TableCell className="font-medium">{dept.name}</TableCell>
<TableCell>
<select
className="w-full border rounded-md px-3 py-1.5 text-sm bg-background"
value={currentSigId}
onChange={(e) => {
const sigId = parseInt(e.target.value);
if (sigId === 0) {
deleteMutation.mutate({ serviceName: dept.name });
} else {
upsertMutation.mutate({ serviceName: dept.name, signatureId: sigId });
}
}}
>
<option value={0}>— Aucune signature —</option>
{signatures.map(sig => (
<option key={sig.id} value={sig.id}>
{sig.firstName} {sig.lastName}
</option>
))}
</select>
</TableCell>
<TableCell>
{assoc && signatures.find(s => s.id === assoc.signatureId) ? (
<div className="h-10 w-24 border rounded overflow-hidden bg-white flex items-center justify-center">
<img
src={signatures.find(s => s.id === assoc.signatureId)!.imageUrl}
alt="aperçu"
className="max-h-full max-w-full object-contain"
/>
</div>
) : (
<span className="text-xs text-muted-foreground italic">Aucune</span>
)}
</TableCell>
<TableCell>
{assoc && (
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700 hover:bg-red-50 h-7 w-7 p-0"
onClick={() => deleteMutation.mutate({ serviceName: dept.name })}
disabled={deleteMutation.isPending}
title="Supprimer l'association"
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
function LlmFieldsConfigSection() {
const { data: fields, isLoading } = trpc.llmFieldsConfig.getAll.useQuery();
const utils = trpc.useUtils();