pini-print-bot / services /pdfService.js
dotandru's picture
feat(v5.7.4): Semantic Closure Gate, Delete unused files, PDF Fixes
d897af7
Raw
History Blame Contribute Delete
7.15 kB
const PDFDocument = require('pdfkit');
const fs = require('fs');
const path = require('path');
/**
* Creates a PDF quote natively using PDFKit.
* This replaces the heavy Puppeteer implementation.
*
* @param {Array} cart - Array of product objects
* @param {Object} customerProfile - Customer details
* @returns {Promise<Buffer>} - Resolves with the PDF file buffer
*/
async function generateQuotePDF(cart, customerProfile, isDraft = false) {
return new Promise((resolve, reject) => {
try {
console.log(`๐Ÿš€ Generating PDF Quote (PDFKit Mode, isDraft: ${isDraft})...`);
const doc = new PDFDocument({ margin: 50, size: 'A4' });
const buffers = [];
doc.on('data', buffers.push.bind(buffers));
doc.on('end', () => {
const pdfData = Buffer.concat(buffers);
console.log("โœ… PDF Generated successfully!");
resolve(pdfData);
});
doc.on('error', (err) => {
reject(err);
});
const fontPath = path.join(__dirname, '../public/fonts/Arimo-Regular.ttf');
const hasHebrewFont = fs.existsSync(fontPath);
const customerName = customerProfile?.name || 'Dear Customer';
const dateStr = new Date().toLocaleDateString('he-IL');
// Header Section
if (hasHebrewFont) doc.font(fontPath);
// Add Logo if exists
let logoPath = path.join(__dirname, '../public/logo.png');
if (!fs.existsSync(logoPath)) {
logoPath = path.join(__dirname, '../public/pini.png'); // Fallback to pini.png
}
if (fs.existsSync(logoPath)) {
doc.image(logoPath, 50, 40, { width: 120 }); // Place logo on the top left
}
doc.fontSize(24).fillColor('#008069').text(reverseHebrew('ื”ืฆืขืช ืžื—ื™ืจ - ื“ืคื•ืก ื‘ื™ืช ื™ืฆื—ืง'), { align: 'center' });
doc.fontSize(14).fillColor('#666666').text(reverseHebrew('ื”ืฆืขืช ืžื—ื™ืจ'), { align: 'center' });
if (isDraft) {
doc.moveDown(1);
doc.fontSize(16).fillColor('#d32f2f').text(reverseHebrew('ื˜ื™ื•ื˜ื” - ื—ืกืจ ืžืคืจื˜ ื˜ื›ื ื™'), { align: 'center' });
doc.fontSize(10).text(reverseHebrew('ื”ืžื—ื™ืจ ื”ืกื•ืคื™ ืขืฉื•ื™ ืœื”ืฉืชื ื•ืช ืขื ื”ืฉืœืžืช ื”ืคืจื˜ื™ื.'), { align: 'center' });
}
doc.moveDown(2);
// Customer Info Box
doc.rect(50, doc.y, 495, 60).fillAndStroke('#f8f9fa', '#eeeeee');
doc.fillColor('#333333').fontSize(12).font(hasHebrewFont ? fontPath : 'Helvetica');
doc.text(reverseHebrew(`ืœื›ื‘ื•ื“: ${customerName}`), 50, doc.y - 45, { align: 'right', width: 480 });
doc.text(reverseHebrew(`ืชืืจื™ืš: ${dateStr}`), 50, doc.y + 5, { align: 'right', width: 480 });
doc.moveDown(3);
const tableTop = doc.y;
doc.fontSize(12).fillColor('#008069');
doc.text('#', 545, tableTop);
doc.text(reverseHebrew('ืžื•ืฆืจ'), 400, tableTop, { align: 'right' });
doc.text(reverseHebrew('ื›ืžื•ืช'), 150, tableTop, { align: 'right' });
doc.text(reverseHebrew('ืžื—ื™ืจ (โ‚ช)'), 50, tableTop, { align: 'right' });
doc.moveTo(50, tableTop + 20).lineTo(545, tableTop + 20).stroke('#dddddd');
let y = tableTop + 30;
doc.font(hasHebrewFont ? fontPath : 'Helvetica').fontSize(10).fillColor('#333333');
// Table Rows
cart.forEach((item, index) => {
// Phase 5.3: Robust Mapping (V3 vs V4)
const isPending = item.isPending === true;
const price = isPending ? 0 : (item.pricing_snapshot?.client_price || item.client_price || 0);
const qty = item.validated_params?.qty || item.qty || 0;
const label = item.displayName || item.product_name || item.product || 'ืคืจื™ื˜';
// Print Row
doc.text(`${index + 1}`, 545, y);
doc.text(reverseHebrew(label), 300, y, { align: 'right', width: 220 });
doc.text(`${qty > 0 ? qty : '-'}`, 150, y, { align: 'right' });
doc.text(isPending ? reverseHebrew('ืžืžืชื™ืŸ') : `โ‚ช${price.toLocaleString()}`, 50, y, { align: 'right' });
y += 15;
// Description (if any)
const desc = item.description || item.validated_params?.paper_type || '';
if (desc) {
doc.fillColor('#888888').fontSize(8);
doc.text(reverseHebrew(desc), 100, y, { width: 420, align: 'right' });
y += doc.heightOfString(desc, { width: 420 }) + 5;
doc.fillColor('#333333').fontSize(10);
} else {
y += 5;
}
// Draw line between items
doc.moveTo(50, y).lineTo(545, y).stroke('#eeeeee');
y += 15;
});
// Total
y += 10;
const total = cart.reduce((sum, i) => sum + (i.isPending ? 0 : (i.pricing_snapshot?.client_price || i.client_price || 0)), 0);
doc.fontSize(14).fillColor('#008069');
doc.text(reverseHebrew(`ืกื”"ื› ืœืชืฉืœื•ื: โ‚ช${total.toLocaleString()}`), 50, y, { align: 'right' });
if (isDraft) {
y += 20;
doc.fontSize(10).fillColor('#d32f2f').text(reverseHebrew('* ื”ืžื—ื™ืจ ื”ืกื•ืคื™ ืขืฉื•ื™ ืœื”ืฉืชื ื•ืช ืขื ื”ืฉืœืžืช ื”ืžืคืจื˜.'), 50, y, { align: 'right' });
}
// Footer & Visual Trust Section (Phase 5.3)
doc.y = 700;
doc.moveTo(50, doc.y).lineTo(545, doc.y).stroke('#008069');
doc.moveDown(1);
doc.fontSize(12).font(hasHebrewFont ? fontPath : 'Helvetica').fillColor('#008069').text(reverseHebrew('ืื–ื•ืจ ืื™ืžื•ืช ื•ืืžื™ื ื•ืช'), { align: 'center' });
doc.fontSize(8).fillColor('#666666');
doc.text(reverseHebrew('โœ“ ืชืงื™ื ื•ืช ืื•ืžืชื” | โœ“ ื ืชื™ื‘ ื”ืคืงื” ืขืงื™ื‘ | โœ“ ืžืคืจื˜ ื ืขื•ืœ'), { align: 'center' });
doc.moveDown(1);
doc.fontSize(7).fillColor('#999999');
cart.forEach(item => {
doc.text(reverseHebrew(`[${item.product}] ืงื•ื“ ืžืขืงื‘: ${item.traceId} | ื’ื™ื‘ื•ื‘: ${item.integrityHash}`), { align: 'center' });
});
doc.y = 800;
doc.fontSize(8).fillColor('#888888').text(reverseHebrew('ื”ื•ืคืง ืขืœ ื™ื“ื™ ืคื™ื ื™ ื”ื‘ื•ื˜ ืขื‘ื•ืจ ื“ืคื•ืก ื‘ื™ืช ื™ืฆื—ืง - ื˜.ืœ.ื—.'), { align: 'center' });
// Finalize PDF file
doc.end();
} catch (error) {
console.error("โŒ PDFKit Error:", error);
reject(error);
}
});
}
/**
* Helper to reverse Hebrew strings for PDFKit if a Unicode font without Bidi is used.
*/
function reverseHebrew(str) {
if (!str) return '';
return str.split('').reverse().join('');
}
module.exports = { generateQuotePDF };