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

@@ -60,6 +60,9 @@ import {
getSignatureById,
createSignature,
deleteSignature,
getServiceSignaturesByUser,
upsertServiceSignature,
deleteServiceSignature,
} from "./db";
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth";
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
@@ -568,11 +571,15 @@ export const appRouter = router({
.mutation(async ({ input, ctx }) => {
const fs = await import('fs/promises');
const path = await import('path');
const { PDFDocument } = await import('pdf-lib');
// Get export folder from settings
const settings = await getImportSettingsByUser(ctx.user.id);
const exportFolder = settings?.exportFolder;
// Load service→signature associations
const serviceAssociations = await getServiceSignaturesByUser(ctx.user.id);
if (!exportFolder) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -622,9 +629,76 @@ export const appRouter = router({
const filename = path.basename(invoice.fileKey);
const destPath = path.join(exportFolder, filename);
// Copy file
await fs.copyFile(sourcePath, destPath);
copiedFiles.push(destPath);
// Find signature associated with invoice service
const serviceName = invoice.serviceConcerne || '';
const assoc = serviceAssociations.find(
a => a.serviceName.toLowerCase() === serviceName.toLowerCase()
);
if (assoc) {
// Load signature image and embed it in the PDF
try {
const sig = await getSignatureById(assoc.signatureId);
if (sig) {
const sigImagePath = path.join(STORAGE_BASE_PATH, sig.imageKey);
const pdfBytes = await fs.readFile(sourcePath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const pages = pdfDoc.getPages();
const lastPage = pages[pages.length - 1];
const { width, height } = lastPage.getSize();
// Read signature image
const sigImageBytes = await fs.readFile(sigImagePath);
const sigImageBase64 = sigImageBytes.toString('base64');
const mimeType = sig.imageKey.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
let embeddedSig;
if (mimeType === 'image/png') {
embeddedSig = await pdfDoc.embedPng(sigImageBytes);
} else {
embeddedSig = await pdfDoc.embedJpg(sigImageBytes);
}
// Draw signature in bottom-right corner
const sigWidth = 120;
const sigHeight = 50;
const margin = 30;
lastPage.drawImage(embeddedSig, {
x: width - sigWidth - margin,
y: margin,
width: sigWidth,
height: sigHeight,
});
// Add signer name below signature
const { StandardFonts } = await import('pdf-lib');
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
lastPage.drawText(`${sig.firstName} ${sig.lastName}`, {
x: width - sigWidth - margin,
y: margin - 14,
size: 9,
font,
});
const signedPdfBytes = await pdfDoc.save();
await fs.writeFile(destPath, signedPdfBytes);
copiedFiles.push(destPath);
console.log(`[Export] Signature apposée pour ${serviceName} sur ${filename}`);
} else {
// Signature not found, copy without signature
await fs.copyFile(sourcePath, destPath);
copiedFiles.push(destPath);
}
} catch (sigError: any) {
console.warn(`[Export] Impossible d'apposer la signature: ${sigError.message}. Copie sans signature.`);
await fs.copyFile(sourcePath, destPath);
copiedFiles.push(destPath);
}
} else {
// No signature association, copy as-is
await fs.copyFile(sourcePath, destPath);
copiedFiles.push(destPath);
}
// Update export status
await updateInvoice(invoice.id, {
@@ -1222,7 +1296,7 @@ export const appRouter = router({
});
}),
delete: protectedProcedure
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
const sig = await getSignatureById(input.id);
@@ -1233,6 +1307,33 @@ export const appRouter = router({
return { success: true };
}),
}),
});
// ============= SERVICE SIGNATURES ROUTES =============
serviceSignatures: router({
list: protectedProcedure.query(async ({ ctx }) => {
return await getServiceSignaturesByUser(ctx.user.id);
}),
upsert: protectedProcedure
.input(z.object({
serviceName: z.string().min(1).max(100),
signatureId: z.number(),
}))
.mutation(async ({ input, ctx }) => {
await upsertServiceSignature({
userId: ctx.user.id,
serviceName: input.serviceName,
signatureId: input.signatureId,
});
return { success: true };
}),
delete: protectedProcedure
.input(z.object({ serviceName: z.string() }))
.mutation(async ({ input, ctx }) => {
await deleteServiceSignature(ctx.user.id, input.serviceName);
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;