Untitled
import PDFDocument from 'pdfkit'; import fs from 'fs'; interface Invoice { invoice?: string; billTo?: string; billToInfo?: any; shipTo?: string; shipToInfo?: any; paymentDate?: string; paymentDateInfo?: string; paymentTerms?: string; paymentTermsInfo?: string; dueDate?: string; dueDateInfo?: string; poNumber?: string; poNumberInfo?: string; amountDue?: string; calc?: { due?: number; subTotal?: number; }; headerBgColor?: string; headerTextColor?: string; 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.'); } 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: PDFKit.PDFDocument, invoice: Invoice): void { doc.image('bg-new.png', 0, 0, { width: 600, fit: [100, 100] }); doc.image('invoice footer-bg.png', 0, 815, { width: 600, fit: [100, 100] }); doc .fillColor('#000000') .fontSize(10) .fontSize(12) .text(`${invoice?.invoice}`, 20, 86, { align: 'right' }) .fontSize(14) .moveDown(); } function generateDynamicSections(doc: PDFKit.PDFDocument, data: Invoice, startX: number, startY: number): number { const sectionSpacing = 152; let currentX = startX; let maxY = startY; const extraSpacing = 4; const flattenObject = (obj: any, parentKey = ''): 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; }, {}); }; const billToInfo = flattenObject(data.billToInfo); let currentY = startY; doc .fillColor('#666666') .font('Helvetica') .fontSize(11) .text(`${data.billTo}`, currentX - 20, currentY); currentY += 15; Object.keys(billToInfo).forEach((key) => { const value = billToInfo[key]; const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`; doc .fillColor('#333333') .font('Helvetica') .fontSize(10) .text(wrappedText, currentX - 20, currentY, { width: 160, lineBreak: true, }); const textHeight = doc.heightOfString(wrappedText, { width: 160 }); currentY += textHeight + extraSpacing; }); maxY = Math.max(maxY, currentY); currentX += sectionSpacing; const shipToInfo = flattenObject(data.shipToInfo); currentY = startY; doc .fillColor('#666666') .font('Helvetica') .fontSize(11) .text(`${data.shipTo}`, currentX, currentY); currentY += 15; Object.keys(shipToInfo).forEach((key) => { const value = shipToInfo[key]; const wrappedText = `${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`; doc .fillColor('#333333') .font('Helvetica') .fontSize(10) .text(wrappedText, currentX, currentY, { width: 160, lineBreak: true, }); const textHeight = doc.heightOfString(wrappedText, { width: 160 }); currentY += textHeight + extraSpacing; }); maxY = Math.max(maxY, currentY); return maxY; } function generateInvoiceDetails(doc: PDFKit.PDFDocument, invoice: Invoice): void { const detailsTop = 130; let currentY = detailsTop; const isEmpty = (value: any): boolean => { return value === null || value === undefined || value === ''; }; 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; } 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; } 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; } 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; } let stockY = 198.5; const rectWidth = 185; const rectHeight = 20; const roundingRadius = 2; doc.roundedRect(390, stockY, rectWidth, rectHeight, roundingRadius); doc.fill(invoice.headerBgColor?.slice(0, 7)); doc.font('Helvetica-Bold'); doc.fillColor(`${invoice.headerTextColor.slice(0, 7)}`); 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: PDFKit.PDFDocument, invoice: Invoice, tableStartY: number): void { let i; const invoiceTableTop = 320; let currentY = 308.5; const rectWidth = 555; const rectHeight = 20; const roundingRadius = 2; doc .roundedRect(20, currentY, rectWidth, rectHeight, roundingRadius) .fill(`${invoice.headerBgColor}`); doc.font('Helvetica-Bold'); doc.fillColor(`${invoice.headerTextColor}`); 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; i++) { const item = invoice.rows![i]; const position = invoiceTableTop + (i + 1) * 24; if (position > doc.page.height - 50) { doc.addPage(); currentY = 50; } generateTableRow( doc, position, item.title, item.quantity, item.rate, item.quantity * item.rate, 'center' ); } const subtotalPosition = invoiceTableTop + (i + 1) * 27; generateHr(doc, subtotalPosition - 15); generateTableInfo( doc, subtotalPosition, '', '', `${invoice.subtotal}`, formatCurrency(invoice.calc?.subTotal || 0) ); let currentPosition = subtotalPosition + 20; if (invoice.shippingInfo) { generateTableInfo( doc, currentPosition, '', '', `${invoice.shipping}`, formatCurrency(invoice.shippingInfo) ); currentPosition += 20; } if (invoice.taxInfo) { generateTableInfo( doc, currentPosition, '', '', `${invoice.tax}`, formatCurrency(invoice.taxInfo) ); currentPosition += 20; } if (invoice.discountInfo) { generateTableInfo( doc, currentPosition, '', '', `${invoice.discount}`, formatCurrency(invoice.discountInfo) ); currentPosition += 20; } if (invoice.totalInfo) { generateTableInfo( doc, currentPosition, '', '', `${invoice.total}`, formatCurrency(invoice.totalInfo) ); currentPosition += 20; } if (invoice.amountPaidInfo) { generateTableInfo( doc, currentPosition, '', '', `${invoice.amountPaid}`, formatCurrency(invoice.amountPaidInfo) ); currentPosition += 20; } const duePosition = currentPosition; doc.font('Helvetica-Bold'); generateTableInfo( doc, duePosition, '', '', `${invoice.amountDue}`, formatCurrency(invoice.calc?.due || 0) ); const tableHeight = duePosition + 20; generateNotesAndTerms(doc, invoice, tableHeight); } function generateTableRow( doc: PDFKit.PDFDocument, y: number, item: string, unitCost: string, quantity: string, lineTotal: string, align: string = 'right' ): void { doc .fontSize(10) .text(item, 30, y) .text(unitCost, 320, y, { width: 90, align: 'center' }) .text(quantity, 420, y, { width: 90, align: 'center' }) .text(lineTotal, 470, y, { align: 'right' }); } function generateTableInfo( doc: PDFKit.PDFDocument, y: number, item: string, unitCost: string, quantity: string, lineTotal: string ): void { doc .fontSize(10) .text(item, 30, y) .text(unitCost, 240, y, { width: 90, align: 'right' }) .text(quantity, 360, y, { width: 90, align: 'right' }) .text(lineTotal, 470, y, { align: 'right' }); } function generateNotesAndTerms(doc: PDFKit.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: PDFKit.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