Checkpoint: Ajout du téléchargement groupé ZIP dans Factures BAP, bouton Re-générer dans Historique BAP, filtres Validées BAP / En attente BAP dans Factures BAP
This commit is contained in:
@@ -66,6 +66,7 @@ import {
|
||||
createBapHistoryEntry,
|
||||
getBapHistoryByUser,
|
||||
deleteBapHistoryEntry,
|
||||
updateBapHistoryPdfUrl,
|
||||
getLearningsByUser,
|
||||
getLearningsBySupplier,
|
||||
upsertLearning,
|
||||
@@ -964,6 +965,133 @@ export const appRouter = router({
|
||||
await deleteBapHistoryEntry(input.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Re-génération du PDF annoté BAP à partir des données en base
|
||||
regenerate: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const entries = await getBapHistoryByUser(ctx.user.id);
|
||||
const entry = entries.find(e => e.id === input.id);
|
||||
if (!entry) throw new TRPCError({ code: 'NOT_FOUND' });
|
||||
|
||||
const invoice = await getInvoiceById(entry.invoiceId);
|
||||
if (!invoice || invoice.userId !== ctx.user.id) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Facture source introuvable' });
|
||||
}
|
||||
|
||||
const fs = await import('fs/promises');
|
||||
const path = await import('path');
|
||||
const { PDFDocument, rgb, StandardFonts } = await import('pdf-lib');
|
||||
const { localStoragePut, generateStorageKey } = await import('./localStorage');
|
||||
|
||||
const importSettings = await getImportSettingsByUser(ctx.user.id);
|
||||
const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), 'storage');
|
||||
|
||||
if (!invoice.fileKey && !invoice.fileUrl) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Fichier PDF source introuvable' });
|
||||
}
|
||||
|
||||
let pdfBytes: Buffer;
|
||||
const sourcePath = path.join(STORAGE_BASE_PATH, invoice.fileKey || '');
|
||||
try {
|
||||
pdfBytes = await fs.readFile(sourcePath);
|
||||
} catch (_) {
|
||||
const fileUrl = invoice.fileUrl;
|
||||
if (!fileUrl) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Fichier PDF source introuvable' });
|
||||
let absoluteUrl = fileUrl;
|
||||
if (fileUrl.startsWith('/')) {
|
||||
const baseUrl = process.env.APP_BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
|
||||
absoluteUrl = `${baseUrl}${fileUrl}`;
|
||||
}
|
||||
const response = await fetch(absoluteUrl);
|
||||
if (!response.ok) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Impossible de télécharger le PDF source' });
|
||||
pdfBytes = Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
const pages = pdfDoc.getPages();
|
||||
const lastPage = pages[pages.length - 1];
|
||||
const { width } = lastPage.getSize();
|
||||
|
||||
const zoneHeight = 160;
|
||||
const zoneX = 30;
|
||||
const zoneY = 10;
|
||||
const zoneW = width - 60;
|
||||
|
||||
lastPage.drawRectangle({
|
||||
x: zoneX, y: zoneY, width: zoneW, height: zoneHeight,
|
||||
color: rgb(1, 1, 1), borderColor: rgb(0.7, 0.7, 0.7), borderWidth: 0.5,
|
||||
});
|
||||
|
||||
const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||
const fontNormal = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const leftColW = Math.floor(zoneW * 0.62);
|
||||
const rightColX = zoneX + leftColW + 10;
|
||||
const rightColW = zoneW - leftColW - 20;
|
||||
|
||||
// Utiliser les données de l'entrée bapHistory
|
||||
const typeAchatText = (entry.typeAchat || 'N/A').toUpperCase();
|
||||
lastPage.drawText(typeAchatText, { x: zoneX + 10, y: zoneY + zoneHeight - 22, size: 11, font, color: rgb(0.1, 0.1, 0.5) });
|
||||
lastPage.drawText('BON À PAYER', { x: zoneX + 10, y: zoneY + zoneHeight - 42, size: 14, font, color: rgb(0, 0.5, 0) });
|
||||
const destinataireText = entry.recipientName || 'TOUS';
|
||||
lastPage.drawText(destinataireText, { x: zoneX + 10, y: zoneY + zoneHeight - 62, size: 10, font: fontNormal, color: rgb(0.2, 0.2, 0.2) });
|
||||
lastPage.drawLine({ start: { x: zoneX + 10, y: zoneY + zoneHeight - 72 }, end: { x: zoneX + leftColW - 10, y: zoneY + zoneHeight - 72 }, thickness: 0.5, color: rgb(0.7, 0.7, 0.7) });
|
||||
const line2 = `Service : ${entry.serviceConcerne || '-'} | Ventilation : ${entry.ventilationComptable || '-'}`;
|
||||
lastPage.drawText(line2, { x: zoneX + 10, y: zoneY + zoneHeight - 88, size: 9, font: fontNormal, color: rgb(0.2, 0.2, 0.2) });
|
||||
const validatedDate = entry.validatedAt ? new Date(entry.validatedAt) : new Date();
|
||||
const dateStr = validatedDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
const timeStr = validatedDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||
lastPage.drawText(`Validé le ${dateStr} à ${timeStr}`, { x: zoneX + 10, y: zoneY + zoneHeight - 104, size: 8, font: fontNormal, color: rgb(0.4, 0.4, 0.4) });
|
||||
lastPage.drawLine({ start: { x: zoneX + leftColW, y: zoneY + 10 }, end: { x: zoneX + leftColW, y: zoneY + zoneHeight - 10 }, thickness: 0.5, color: rgb(0.8, 0.8, 0.8) });
|
||||
|
||||
// Signature
|
||||
let signatureName: string | null = entry.signatureName || null;
|
||||
if (entry.serviceConcerne) {
|
||||
const serviceAssociations = await getServiceSignaturesByUser(ctx.user.id);
|
||||
const assoc = serviceAssociations.find(a => a.serviceName.toLowerCase() === (entry.serviceConcerne || '').toLowerCase());
|
||||
if (assoc) {
|
||||
const sig = await getSignatureById(assoc.signatureId);
|
||||
if (sig) {
|
||||
signatureName = `${sig.firstName} ${sig.lastName}`;
|
||||
try {
|
||||
let sigImageBytes: Buffer;
|
||||
const sigImagePath = path.join(STORAGE_BASE_PATH, sig.imageKey);
|
||||
try { sigImageBytes = await fs.readFile(sigImagePath); } catch (_) {
|
||||
const sigUrl = sig.imageUrl;
|
||||
if (!sigUrl) throw new Error('Image signature introuvable');
|
||||
let absoluteSigUrl = sigUrl;
|
||||
if (sigUrl.startsWith('/')) {
|
||||
const baseUrl = process.env.APP_BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
|
||||
absoluteSigUrl = `${baseUrl}${sigUrl}`;
|
||||
}
|
||||
const sigResp = await fetch(absoluteSigUrl);
|
||||
if (!sigResp.ok) throw new Error('Impossible de télécharger la signature');
|
||||
sigImageBytes = Buffer.from(await sigResp.arrayBuffer());
|
||||
}
|
||||
const mimeType = sig.imageKey.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||
const embeddedSig = mimeType === 'image/png' ? await pdfDoc.embedPng(sigImageBytes) : await pdfDoc.embedJpg(sigImageBytes);
|
||||
const sigWidth = Math.min(rightColW - 10, 110);
|
||||
const sigHeight = Math.round(sigWidth * 0.45);
|
||||
const sigX = rightColX + (rightColW - sigWidth) / 2;
|
||||
lastPage.drawImage(embeddedSig, { x: sigX, y: zoneY + 35, width: sigWidth, height: sigHeight });
|
||||
const nameW = fontNormal.widthOfTextAtSize(signatureName, 8);
|
||||
lastPage.drawText(signatureName, { x: sigX + (sigWidth - nameW) / 2, y: zoneY + 22, size: 8, font: fontNormal, color: rgb(0.3, 0.3, 0.3) });
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const signedPdfBytes = await pdfDoc.save();
|
||||
const filename = path.basename(invoice.fileKey || `invoice_${invoice.id}.pdf`);
|
||||
const bapFilename = `BAP_${Date.now()}_${filename}`;
|
||||
const bapKey = generateStorageKey(ctx.user.id, bapFilename);
|
||||
const { url } = await localStoragePut(bapKey, Buffer.from(signedPdfBytes), 'application/pdf');
|
||||
|
||||
// Mettre à jour l'entrée bapHistory avec le nouveau pdfUrl
|
||||
await updateBapHistoryPdfUrl(input.id, url);
|
||||
|
||||
return { success: true, pdfUrl: url };
|
||||
}),
|
||||
}),
|
||||
|
||||
// ============= SOURCE FILES ROUTES =============
|
||||
|
||||
Reference in New Issue
Block a user