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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user