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:
@@ -51,7 +51,22 @@ function downloadBapPdf(
|
||||
export default function BapHistory() {
|
||||
const { data: entries, isLoading, refetch } = trpc.bapHistory.getAll.useQuery();
|
||||
const deleteMutation = trpc.bapHistory.delete.useMutation();
|
||||
const regenerateMutation = trpc.bapHistory.regenerate.useMutation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [regeneratingId, setRegeneratingId] = useState<number | null>(null);
|
||||
|
||||
const handleRegenerate = async (id: number) => {
|
||||
setRegeneratingId(id);
|
||||
try {
|
||||
const result = await regenerateMutation.mutateAsync({ id });
|
||||
toast.success('PDF re-généré avec succès');
|
||||
refetch();
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Erreur lors de la re-génération du PDF');
|
||||
} finally {
|
||||
setRegeneratingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = (entries || []).filter((e) => {
|
||||
const q = search.toLowerCase();
|
||||
@@ -255,19 +270,43 @@ export default function BapHistory() {
|
||||
</Button>
|
||||
</div>
|
||||
) : entry.exportPath ? (
|
||||
<div className="flex items-center gap-1 text-orange-600">
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
<span className="text-xs truncate max-w-[100px]" title={entry.exportPath}>
|
||||
Dossier
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 text-orange-600">
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
<span className="text-xs truncate max-w-[80px]" title={entry.exportPath}>Dossier</span>
|
||||
</div>
|
||||
{/* Bouton Re-générer si le PDF de dossier n'est plus accessible */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-violet-600 hover:text-violet-700 hover:bg-violet-50"
|
||||
onClick={() => handleRegenerate(entry.id)}
|
||||
disabled={regeneratingId === entry.id}
|
||||
title="Re-générer le PDF annoté"
|
||||
>
|
||||
{regeneratingId === entry.id ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground text-xs">—</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground text-xs">—</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-violet-600 hover:text-violet-700 hover:bg-violet-50"
|
||||
onClick={() => handleRegenerate(entry.id)}
|
||||
disabled={regeneratingId === entry.id}
|
||||
title="Re-générer le PDF annoté"
|
||||
>
|
||||
{regeneratingId === entry.id ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5 mr-1" />}
|
||||
<span className="text-xs">Re-générer</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Bouton Réexporter */}
|
||||
{/* Bouton Réexporter (re-télécharger) */}
|
||||
{entry.pdfUrl && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -280,6 +319,19 @@ export default function BapHistory() {
|
||||
<span className="text-xs">Réexporter</span>
|
||||
</Button>
|
||||
)}
|
||||
{/* Bouton Re-générer (recréer le PDF annoté depuis la base) */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-violet-600 hover:text-violet-700 hover:bg-violet-50"
|
||||
onClick={() => handleRegenerate(entry.id)}
|
||||
disabled={regeneratingId === entry.id}
|
||||
title="Re-générer le PDF annoté depuis les données en base"
|
||||
>
|
||||
{regeneratingId === entry.id
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
: <RefreshCw className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
{/* Bouton Supprimer */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -32,7 +32,32 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw } from "lucide-react";
|
||||
import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw, FolderDown } from "lucide-react";
|
||||
|
||||
// Helper : télécharge un ZIP de plusieurs PDFs annotés BAP
|
||||
async function downloadBapZip(
|
||||
files: Array<{ pdfPath: string; filename: string }>
|
||||
) {
|
||||
const response = await fetch('/api/download-bap-zip', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ files }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error((err as any).error || 'Erreur lors de la génération du ZIP');
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const zipFilename = `BAP_export_${new Date().toLocaleDateString('fr-CA')}.zip`;
|
||||
a.download = zipFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Helper : télécharge un PDF annoté BAP depuis son URL de stockage
|
||||
// Nommage : AAAA-MM-JJ - Fournisseur - N°Facture.pdf
|
||||
@@ -95,6 +120,7 @@ export default function InvoicesBAP() {
|
||||
const [bapPdfUrls, setBapPdfUrls] = useState<Record<number, string>>({});
|
||||
const [textDialogOpen, setTextDialogOpen] = useState(false);
|
||||
const [selectedInvoiceText, setSelectedInvoiceText] = useState<string | null>(null);
|
||||
const [isZipDownloading, setIsZipDownloading] = useState(false);
|
||||
const { data: allInvoices, isLoading } = trpc.invoices.list.useQuery();
|
||||
// Filter for BAP invoices only (Abonnement = NON, isSubscription = 0)
|
||||
const invoices = allInvoices?.filter(inv => inv.isSubscription === 0);
|
||||
@@ -299,6 +325,8 @@ export default function InvoicesBAP() {
|
||||
if (statusFilter === "exported" && inv.exportStatus !== "exported") return false;
|
||||
if (statusFilter === "not_exported" && inv.exportStatus !== "not_exported") return false;
|
||||
if (statusFilter === "export_error" && inv.exportStatus !== "export_error") return false;
|
||||
if (statusFilter === "bap_validated" && inv.bapValidated !== 1) return false;
|
||||
if (statusFilter === "bap_pending" && inv.bapValidated === 1) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -309,6 +337,8 @@ export default function InvoicesBAP() {
|
||||
exported: invoices?.filter(inv => inv.exportStatus === "exported").length || 0,
|
||||
not_exported: invoices?.filter(inv => inv.exportStatus === "not_exported").length || 0,
|
||||
export_error: invoices?.filter(inv => inv.exportStatus === "export_error").length || 0,
|
||||
bap_validated: invoices?.filter(inv => inv.bapValidated === 1).length || 0,
|
||||
bap_pending: invoices?.filter(inv => inv.bapValidated !== 1).length || 0,
|
||||
};
|
||||
|
||||
// Old filter logic (to be removed)
|
||||
@@ -455,6 +485,43 @@ export default function InvoicesBAP() {
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exporter ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
// Construire la liste des PDFs validés parmi la sélection
|
||||
const files = selectedIds
|
||||
.map(id => {
|
||||
const inv = invoices?.find(i => i.id === id);
|
||||
const pdfUrl = allBapPdfUrls[id];
|
||||
if (!inv || !pdfUrl) return null;
|
||||
const datePart = new Date().toLocaleDateString('fr-CA');
|
||||
const supplierPart = (inv.supplierName || '').replace(/[^a-zA-Z0-9\u00e0-\u00ff \-]/g, '').trim();
|
||||
const numberPart = (inv.invoiceNumber || '').replace(/[^a-zA-Z0-9\-]/g, '').trim();
|
||||
const parts = [datePart, supplierPart, numberPart].filter(Boolean);
|
||||
const filename = parts.length > 0 ? `${parts.join(' - ')}.pdf` : pdfUrl.split('/').pop() || 'BAP.pdf';
|
||||
return { pdfPath: pdfUrl, filename };
|
||||
})
|
||||
.filter(Boolean) as Array<{ pdfPath: string; filename: string }>;
|
||||
if (!files.length) {
|
||||
toast.error('Aucune facture validée BAP parmi la sélection');
|
||||
return;
|
||||
}
|
||||
setIsZipDownloading(true);
|
||||
try {
|
||||
await downloadBapZip(files);
|
||||
toast.success(`ZIP généré avec ${files.length} PDF(s)`);
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Erreur lors de la génération du ZIP');
|
||||
} finally {
|
||||
setIsZipDownloading(false);
|
||||
}
|
||||
}}
|
||||
disabled={selectedIds.length === 0 || isZipDownloading}
|
||||
variant="outline"
|
||||
className="border-indigo-500 text-indigo-600 hover:bg-indigo-50"
|
||||
>
|
||||
<FolderDown className={`w-4 h-4 mr-2 ${isZipDownloading ? 'animate-bounce' : ''}`} />
|
||||
{isZipDownloading ? 'ZIP...' : `ZIP (${selectedIds.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (confirm(`Voulez-vous vraiment supprimer ${selectedIds.length} facture(s) ?`)) {
|
||||
@@ -560,6 +627,22 @@ export default function InvoicesBAP() {
|
||||
>
|
||||
Erreurs ({statusCounts.export_error})
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "bap_validated" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("bap_validated")}
|
||||
size="sm"
|
||||
className={statusFilter === "bap_validated" ? "bg-emerald-600 hover:bg-emerald-700" : ""}
|
||||
>
|
||||
✅ Validées BAP ({statusCounts.bap_validated})
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "bap_pending" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("bap_pending")}
|
||||
size="sm"
|
||||
className={statusFilter === "bap_pending" ? "bg-orange-600 hover:bg-orange-700" : ""}
|
||||
>
|
||||
⏳ En attente BAP ({statusCounts.bap_pending})
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
|
||||
@@ -47,12 +47,14 @@
|
||||
"@trpc/client": "^11.6.0",
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@trpc/server": "^11.6.0",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/chokidar": "^2.1.7",
|
||||
"@types/imap": "^0.8.43",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"@types/ssh2-sftp-client": "^9.0.6",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
|
||||
620
pnpm-lock.yaml
generated
620
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import { createServer } from "http";
|
||||
import net from "net";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import archiver from "archiver";
|
||||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
||||
import { registerOAuthRoutes } from "./oauth";
|
||||
import { appRouter } from "../routers";
|
||||
@@ -97,6 +98,47 @@ async function startServer() {
|
||||
res.sendFile(found);
|
||||
});
|
||||
|
||||
// Route de téléchargement groupé ZIP des PDFs annotés BAP
|
||||
// POST /api/download-bap-zip avec body { files: Array<{ pdfPath: string, filename: string }> }
|
||||
app.post("/api/download-bap-zip", (req, res) => {
|
||||
const files: Array<{ pdfPath: string; filename: string }> = req.body.files || [];
|
||||
if (!files.length) {
|
||||
res.status(400).json({ error: "Aucun fichier spécifié" });
|
||||
return;
|
||||
}
|
||||
// Vérifier que tous les chemins sont dans storage/
|
||||
const resolvedFiles: Array<{ absPath: string; filename: string }> = [];
|
||||
for (const f of files) {
|
||||
const normalized = path.normalize(f.pdfPath).replace(/^\/+/, '');
|
||||
if (normalized.startsWith('..') || !normalized.startsWith('storage')) continue;
|
||||
const absPath = path.resolve(normalized);
|
||||
if (fs.existsSync(absPath)) {
|
||||
resolvedFiles.push({ absPath, filename: f.filename });
|
||||
}
|
||||
}
|
||||
if (!resolvedFiles.length) {
|
||||
res.status(404).json({ error: "Aucun fichier trouvé" });
|
||||
return;
|
||||
}
|
||||
const zipFilename = `BAP_export_${new Date().toLocaleDateString('fr-CA')}.zip`;
|
||||
const encodedZip = encodeURIComponent(zipFilename);
|
||||
res.setHeader("Content-Type", "application/zip");
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${encodedZip}"; filename*=UTF-8''${encodedZip}`);
|
||||
const archive = archiver('zip', { zlib: { level: 6 } });
|
||||
archive.on('error', (err) => { console.error('ZIP error:', err); res.destroy(); });
|
||||
archive.pipe(res);
|
||||
// Gérer les doublons de noms de fichiers
|
||||
const usedNames = new Map<string, number>();
|
||||
for (const { absPath, filename } of resolvedFiles) {
|
||||
const base = filename.replace(/\.pdf$/i, '');
|
||||
const count = usedNames.get(base) || 0;
|
||||
usedNames.set(base, count + 1);
|
||||
const finalName = count === 0 ? filename : `${base} (${count}).pdf`;
|
||||
archive.file(absPath, { name: finalName });
|
||||
}
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
// tRPC API
|
||||
app.use(
|
||||
"/api/trpc",
|
||||
|
||||
@@ -919,3 +919,10 @@ export async function getBapPdfUrlsByInvoiceIds(invoiceIds: number[]): Promise<R
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// Mettre à jour le pdfUrl d'une entrée bapHistory (après re-génération)
|
||||
export async function updateBapHistoryPdfUrl(id: number, pdfUrl: string): Promise<void> {
|
||||
const db = await getDb();
|
||||
if (!db) return;
|
||||
await db.update(bapHistory).set({ pdfUrl }).where(eq(bapHistory.id, id));
|
||||
}
|
||||
|
||||
@@ -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