Untitled
import PDFDocument from 'pdfkit'; import fs from 'fs'; interface Invoice { companyName?: string; invoice?: string; billTo?: string; billToInfo?: Record<string, any>; shipTo?: string; shipToInfo?: Record<string, any>; paymentDate?: string; paymentDateInfo?: string; paymentTerms?: string; paymentTermsInfo?: string; dueDate?: string; dueDateInfo?: string; poNumber?: string; poNumberInfo?: string; headerTextColor?: string; headerBgColor?: string; amountDue?: string; calc?: { due?: number; subTotal?: number; }; item?: string; quantity?: string; rate?: string; amount?: string; rows?: Array<{ title: string; quantity: number; rate: number; }>; subtotal?: string; shipping?: string; shippingInfo?: number; tax?: string; taxInfo?: number; discount?: string; discountInfo?: number; total?: string; totalInfo?: number; amountPaid?: string; amountPaidInfo?: number; note?: string; noteInfo?: string; terms?: string; termsInfo?: string; } function createInvoice(invoice: Invoice, outputPath: string): void { if (!invoice || typeof invoice !== 'object') { throw new Error('Invalid input invoice provided.'); } console.log(invoice, 'invoice'); const doc = new PDFDocument({ margin: 35, size: 'A4' }); doc.pipe(fs.createWriteStream(outputPath)); doc.font('Helvetica'); generateHeader(doc, invoice); const dynamicSectionsEndY = generateDynamicSections(doc, invoice, 50, 140); const tableStartY = Math.max(dynamicSectionsEndY, 300); generateInvoiceDetails(doc, invoice); generateInvoiceTable(doc, invoice, tableStartY); doc.end(); } function generateHeader(doc: PDFDocument, invoice: Invoice): void { doc .image('rootdevs-logo.png', 20, 25, { width: 160, fit: [100, 100] }) .fillColor('#444444') .fontSize(10) .text(`${invoice?.companyName}`, 30, 70, { width: 350, }) .fontSize(25) .text(`${invoice?.invoice}`, 20, 36, { align: 'right' }) .fontSize(14) .moveDown(); } function generateDynamicSections(doc: PDFDocument, data: Invoice, startX: number, startY: number): number { const sectionSpacing = 152; let currentX = startX; let maxY = startY; const extraSpacing = 4; // Add extra space between fields // Helper function to flatten nested objects const flattenObject = (obj: Record<string, any>, parentKey = ''): Record<string, any> => { return Object.keys(obj).reduce((acc, key) => { const newKey = parentKey ? `${parentKey}.${key}` : key; if (typeof obj[key] === 'object' && obj[key] !== null) { Object.assign(acc, flattenObject(obj[key], newKey)); } else { acc[newKey] = obj[key]; } return acc; }, {}); }; // Process billToInfo const billToInfo = flattenObject(data.billToInfo || {}); let currentY = startY; // Write section name for Bill To doc .fillColor('#666666') .font('Helvetica') .fontSize(11) .text(`${data.billTo}`, currentX - 20, currentY); currentY += 15; // Process each field in billToInfo Object.keys(billToInfo).forEach((key) => { const value = billToInfo[key]; // Write field and value with text wrapping const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`; doc .fillColor('#333333') .font('Helvetica') .fontSize(10) .text(wrappedText, currentX - 20, currentY, { width: 160, // Wrap text to the specified width lineBreak: true, // Enable line wrapping for long text }); // Calculate the height of the text block const textHeight = doc.heightOfString(wrappedText, { width: 160 }); // Update currentY based on the actual height of the text block currentY += textHeight + extraSpacing; // Add extra space after each field }); // Update maxY based on the last field's Y position maxY = Math.max(maxY, currentY); // Move to the next section (Ship To) currentX += sectionSpacing; // Process shipToInfo const shipToInfo = flattenObject(data.shipToInfo || {}); currentY = startY; // Write section name for Ship To doc .fillColor('#666666') .font('Helvetica') .fontSize(11) .text(`${data.shipTo}`, currentX, currentY); currentY += 15; // Process each field in shipToInfo Object.keys(shipToInfo).forEach((key) => { const value = shipToInfo[key]; // Write field and value with text wrapping const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`; doc .fillColor('#333333') .font('Helvetica') .fontSize(10) .text(wrappedText, currentX, currentY, { width: 160, // Wrap text to the specified width lineBreak: true, // Enable line wrapping for long text }); // Calculate the height of the text block const textHeight = doc.heightOfString(wrappedText, { width: 160 }); // Update currentY based on the actual height of the text block currentY += textHeight + extraSpacing; // Add extra space after each field }); // Update maxY based on the last field's Y position maxY = Math.max(maxY, currentY); return maxY; } function generateInvoiceDetails(doc: PDFDocument, invoice: Invoice): void { const detailsTop = 130; let currentY = detailsTop; // Helper function to check if a value is empty const isEmpty = (value: any): boolean => { return value === null || value === undefined || value === ''; }; // Render fields only if both the label and value are not empty if (!isEmpty(invoice?.paymentDate) && !isEmpty(invoice.paymentDateInfo)) { doc .fontSize(10) .fillColor('#666666') .text(`${invoice.paymentDate}`, 0, currentY, { width: 470, align: 'right', }) .fillColor('#000000') .text(formatDate(new Date(invoice.paymentDateInfo!)), 0, currentY, { width: 550, align: 'right', }); currentY += 25; // Move to the next line } if (!isEmpty(invoice?.paymentTerms) && !isEmpty(invoice.paymentTermsInfo)) { doc .fontSize(10) .fillColor('#666666') .text(`${invoice.paymentTerms}`, 0, currentY, { width: 470, align: 'right', }) .fillColor('#000000') .text(`${invoice.paymentTermsInfo}`, 0, currentY, { width: 550, align: 'right', }); currentY += 25; // Move to the next line } if (!isEmpty(invoice?.dueDate) && !isEmpty(invoice.dueDateInfo)) { doc .fontSize(10) .fillColor('#666666') .text(`${invoice.dueDate}`, 0, currentY, { width: 470, align: 'right', }) .fillColor('#000000') .text(formatDate(new Date(invoice.dueDateInfo!)), 0, currentY, { width: 550, align: 'right', }); currentY += 25; // Move to the next line } if (!isEmpty(invoice?.poNumber) && !isEmpty(invoice.poNumberInfo)) { doc .fontSize(10) .fillColor('#666666') .text(`${invoice.poNumber}`, 0, currentY, { width: 470, align: 'right', }) .fillColor('#000000') .text(`${invoice.poNumberInfo}`, 0, currentY, { width: 550, align: 'right', }); currentY += 25; // Move to the next line } // Draw the line doc .fillColor(`${invoice.headerTextColor?.slice(0, 7)}`) .lineWidth(20) .lineCap('round') .moveTo(560, currentY + 4) .lineTo(390, currentY + 4) .strokeColor(`${invoice.headerBgColor?.slice(0, 7)}`) .stroke(); // Render amount due only if it is not empty if (!isEmpty(invoice?.amountDue) && !isEmpty(invoice.calc?.due)) { doc .font('Helvetica-Bold') .text(`${invoice.amountDue}`, 0, currentY, { width: 470, align: 'right', }) .text(`${invoice.calc.due}`, 0, currentY, { width: 550, align: 'right', }); } } function generateInvoiceTable(doc: PDFDocument, invoice: Invoice, tableStartY: number): void { let i; const invoiceTableTop = 320; doc.font('Helvetica-Bold'); doc .lineWidth(25) .lineCap('round') .moveTo(25, 318) .lineTo(565, 318) .strokeColor(`${invoice.headerBgColor?.slice(0, 7)}`) .stroke(); doc.fillColor(`${invoice.headerTextColor?.slice(0, 7)}`); generateTableRow( doc, invoiceTableTop - 5, `${invoice.item}`, `${invoice.quantity}`, `${invoice.rate}`, `${invoice.amount}` ); doc.fillColor('#000000'); doc.font('Helvetica'); for (i = 0; i < (invoice.rows?.length || 0); i++) { const item = invoice.rows![i]; const position = invoiceTableTop + (i + 1) * 24; generateTableRow( doc, position, item.title, item.quantity, item.rate, item.quantity * item.rate ); } const subtotalPosition = invoiceTableTop + (i + 1) * 27; generateHr(doc, subtotalPosition - 15); generateTableRow( doc, subtotalPosition, '', '', `${invoice.subtotal}`, formatCurrency(invoice.calc?.subTotal || 0) ); let currentPosition = subtotalPosition + 20; if (invoice.shippingInfo) { generateTableRow( doc, currentPosition, '', '', `${invoice.shipping}`, formatCurrency(invoice.shippingInfo) ); currentPosition += 20; } if (invoice.taxInfo) { generateTableRow( doc, currentPosition, '', '', `${invoice.tax}`, formatCurrency(invoice.taxInfo) ); currentPosition += 20; } if (invoice.discountInfo) { generateTableRow( doc, currentPosition, '', '', `${invoice.discount}`, formatCurrency(invoice.discountInfo) ); currentPosition += 20; } if (invoice.totalInfo) { generateTableRow( doc, currentPosition, '', '', `${invoice.total}`, formatCurrency(invoice.totalInfo) ); currentPosition += 20; } if (invoice.amountPaidInfo) { generateTableRow( doc, currentPosition, '', '', `${invoice.amountPaid}`, formatCurrency(invoice.amountPaidInfo) ); currentPosition += 20; } const duePosition = currentPosition; doc.font('Helvetica-Bold'); generateTableRow( doc, duePosition, '', '', `${invoice.amountDue}`, formatCurrency(invoice.calc?.due || 0) ); const tableHeight = duePosition + 20; generateNotesAndTerms(doc, invoice, tableHeight); } function generateTableRow(doc: PDFDocument, y: number, item: string, unitCost: string, quantity: string, lineTotal: string): void { doc .fontSize(10) .text(item, 30, y) .text(unitCost, 250, y, { width: 90, align: 'right' }) .text(quantity, 350, y, { width: 90, align: 'right' }) .text(lineTotal, 420, y, { align: 'right' }); } function generateNotesAndTerms(doc: PDFDocument, invoice: Invoice, tableHeight: number): void { let notePosition = tableHeight + 20; const lorem1Height = doc.heightOfString(`${invoice?.noteInfo}`, { width: 500, }); if (invoice.noteInfo) { doc.fontSize(10).text(`${invoice.note}`, 25, notePosition); doc.font('Helvetica').text(`${invoice.noteInfo}`, 25, notePosition + 15); } const termsPosition = notePosition + lorem1Height + 20; if (invoice.termsInfo) { doc .fontSize(10) .font('Helvetica-Bold') .text(`${invoice.terms}`, 25, termsPosition); doc.font('Helvetica').text(`${invoice.termsInfo}`, 25, termsPosition + 15); } } function formatCurrency(amount: number): string { return `$${Number(amount)?.toFixed(2)}`; } function generateHr(doc: PDFDocument, y: number): void { doc .strokeColor('#06243E') .lineWidth(0.1) .moveTo(27, y) .lineTo(562, y) .stroke(); } function formatDate(date: Date): string { const day = date.getDate(); const month = date.getMonth() + 1; const year = date.getFullYear(); return `${year}/${month}/${day}`; } export { createInvoice };
Leave a Comment